Documentation
¶
Overview ¶
Package access holds the portal's authorization core: one checker that answers every "may this user see or change this thing" question the portal asks, over assets, collections, prompts, knowledge pages and threads.
Before this package the answers were methods scattered across the handler files, reachable only from *Handler. Splitting the handler layer into seams made that untenable: a seam would have had to receive each check as an injected func, which is not a seam but a second copy of the handler's dependency graph. The checks are also the portal's security boundary, so a single implementation both the parent and the seams call is what keeps a permission from quietly meaning two things in two places.
Every method is pure with respect to HTTP: it answers the question and never writes a response. Callers that must deny with a specific status keep their own thin wrapper, so the status and message wording stay with the route.
Index ¶
- Constants
- func AssetOwnerOf(user *User) portaldomain.AssetOwner
- func ContextWithUser(ctx context.Context, user *User) context.Context
- func GrantsEdit(perm portaldomain.SharePermission) bool
- func HasAnyRole(userRoles, targetRoles []string) bool
- func IsShareActive(s portaldomain.Share) bool
- func OwnsAsset(asset *portaldomain.Asset, user *User) bool
- type Checker
- func (c *Checker) AssetSharePermission(ctx context.Context, assetID string, user *User) (portaldomain.SharePermission, error)
- func (c *Checker) AssetViewGrant(ctx context.Context, assetID string, asset *portaldomain.Asset, user *User) (bool, error)
- func (c *Checker) CanEditAssetSilent(ctx context.Context, assetID string, user *User) bool
- func (c *Checker) CanEditCollection(ctx context.Context, coll *portaldomain.Collection, user *User) bool
- func (c *Checker) CanEditCollectionSilent(ctx context.Context, collectionID string, user *User) bool
- func (c *Checker) CanManage(ownerID string, user *User) bool
- func (c *Checker) CanManageAsset(asset *portaldomain.Asset, user *User) bool
- func (c *Checker) CanManageEmail(ownerEmail string, user *User) bool
- func (c *Checker) CanModerateThread(ctx context.Context, user *User, thread *threads.Thread) bool
- func (c *Checker) CanViewAsset(ctx context.Context, assetID string, asset *portaldomain.Asset, user *User) bool
- func (c *Checker) CanViewCollection(ctx context.Context, coll *portaldomain.Collection, user *User) bool
- func (c *Checker) CanViewPrompt(ctx context.Context, user *User, pr *prompt.Prompt) bool
- func (c *Checker) CollectionSharePermission(ctx context.Context, collectionID string, user *User) portaldomain.SharePermission
- func (c *Checker) HasApplyKnowledge(user *User) bool
- func (c *Checker) HasTool(user *User, tool string) bool
- func (c *Checker) IsAdmin(user *User) bool
- func (c *Checker) OwnedAssetIDs(ctx context.Context, ids []string, user *User) []string
- func (c *Checker) OwnedCollectionIDs(ctx context.Context, ids []string, user *User) []string
- func (c *Checker) OwnedTargetIDs(ctx context.Context, targetType string, ids []string, user *User) []string
- func (c *Checker) OwnsPersonalPrompt(ctx context.Context, promptID string, user *User) bool
- func (c *Checker) ResolveAssetPermission(ctx context.Context, assetID string, user *User) (portaldomain.SharePermission, error)
- type Config
- type User
Constants ¶
const ApplyKnowledgeTool = "apply_knowledge"
ApplyKnowledgeTool is the persona tool whose access gates insight review and canonical-knowledge writes. It is the single capability the REST path checks, matching the MCP path's persona tool-visibility gate so both agree on who may promote knowledge.
Variables ¶
This section is empty.
Functions ¶
func AssetOwnerOf ¶ added in v1.127.0
func AssetOwnerOf(user *User) portaldomain.AssetOwner
AssetOwnerOf is the ownership identity a portal user is judged by: the id they authenticated with and their address.
The address is a second key, not a fallback. A managed script's output is stamped with the script principal as its owner id and the script owner's address as owner_email, so a check on the id alone leaves the person the output was produced for unable to see it (#1551). It is a no-op on an asset a person saved themselves, whose two identifiers name the same person.
func ContextWithUser ¶
ContextWithUser returns a copy of ctx carrying the authenticated user, the value GetUser reads. The key is unexported and lives here with the subject of every permission check, so the parent package and every handler seam read the identity the portal authenticator wrote rather than each keying its own copy.
func GrantsEdit ¶ added in v1.121.0
func GrantsEdit(perm portaldomain.SharePermission) bool
GrantsEdit reports whether a resolved share permission carries edit rights. It is the one place the Editor level is compared, so a caller that already holds a resolved permission — getCollection reports one alongside the record it is answering with — applies the same rule CanEditCollection applies, without repeating the comparison or paying for a second lookup.
func HasAnyRole ¶
HasAnyRole returns true if any role in userRoles is also in targetRoles. It is exported for the few callers that hold a role list rather than a Checker (the prompt mutation gate takes its admin roles as an argument).
func IsShareActive ¶
func IsShareActive(s portaldomain.Share) bool
IsShareActive returns true if the share is not revoked and not expired.
func OwnsAsset ¶ added in v1.127.0
func OwnsAsset(asset *portaldomain.Asset, user *User) bool
OwnsAsset reports whether the asset belongs to the user, on either of the two identifiers it records. It is the one judgment of "this asset is this person's" the portal makes, so an owner-only affordance and an owner-scoped listing cannot come to different answers about the same row.
Types ¶
type Checker ¶
type Checker struct {
// contains filtered or unexported fields
}
Checker answers the portal's authorization questions.
func (*Checker) AssetSharePermission ¶
func (c *Checker) AssetSharePermission(ctx context.Context, assetID string, user *User) (portaldomain.SharePermission, error)
AssetSharePermission returns the highest permission level a user has for a shared asset. It returns the empty permission if the asset is not shared with this user.
func (*Checker) AssetViewGrant ¶
func (c *Checker) AssetViewGrant(ctx context.Context, assetID string, asset *portaldomain.Asset, user *User) (bool, error)
AssetViewGrant reports whether the user may view the asset, distinguishing a denial from a failure to determine one. A non-nil error means the direct-share lookup failed and no collection grant covered for it, which callers surface as 500 rather than 403.
func (*Checker) CanEditAssetSilent ¶
CanEditAssetSilent reports owner-or-admin-or-editor access to an asset. A missing or soft-deleted asset denies.
func (*Checker) CanEditCollection ¶ added in v1.121.0
func (c *Checker) CanEditCollection(ctx context.Context, coll *portaldomain.Collection, user *User) bool
CanEditCollection reports whether the user may change the collection itself: its name, description, settings, sections and thumbnail. It grants to the owner, an admin, and the holder of an Editor share on the collection.
The Editor arm is what makes an Editor share on a collection mean anything about the collection rather than only about the assets inside it (#1294): a person trusted to rewrite the contents of every asset in a collection is not a plausible person to refuse a fix to the collection's own title. Deleting a collection, sharing it, and reading its share list stay on CanManage — destruction and re-granting access are owner rights, not editing rights, so an Editor deliberately holds neither.
func (*Checker) CanEditCollectionSilent ¶
func (c *Checker) CanEditCollectionSilent(ctx context.Context, collectionID string, user *User) bool
CanEditCollectionSilent is CanEditCollection for callers that hold only an ID. A missing or soft-deleted collection denies.
func (*Checker) CanManage ¶ added in v1.121.0
CanManage reports whether the user holds owner authority over a thing owned by ownerID: the owner, or an admin.
It is the single seam behind every "only the owner can ..." gate the portal applies to assets and collections, so admin reach is decided once instead of being re-derived at each route. Admin only ever widens here, and never grants more than the admin already has: an admin reads, edits and deletes any asset through the admin API, so refusing them the weaker rights to share it, list its shares, or revoke one was an artifact of the gate being written as a bare ID comparison rather than a decision (#1293).
Assets use CanManageAsset instead: their ownership is recorded as an id and an address, and only the pair identifies the person a managed script's output was produced for (#1551).
func (*Checker) CanManageAsset ¶ added in v1.127.0
func (c *Checker) CanManageAsset(asset *portaldomain.Asset, user *User) bool
CanManageAsset reports whether the user holds owner authority over the asset: its owner, or an admin. It is CanManage for assets, which record ownership as an id and an address rather than as an id alone.
func (*Checker) CanManageEmail ¶ added in v1.121.0
CanManageEmail is CanManage for the entities whose ownership is recorded as an email address rather than a user ID (prompts). The address comparison is case-insensitive, matching OwnsPersonalPrompt and the share-recipient match, because addresses reach the platform from several identity providers.
func (*Checker) CanModerateThread ¶
CanModerateThread reports whether the user may change a thread's status or delete it: the thread author, an admin, or an owner/editor of the target. Standalone threads are moderated only by their author or an admin.
func (*Checker) CanViewAsset ¶
func (c *Checker) CanViewAsset(ctx context.Context, assetID string, asset *portaldomain.Asset, user *User) bool
CanViewAsset reports whether the user may view the asset (owner, a direct share, or a collection share). A direct-share store error is tolerated: a collection grant still allows access. It short-circuits on a direct grant to avoid a collection query on the hot path, where callers resolve many assets in a loop.
func (*Checker) CanViewCollection ¶
func (c *Checker) CanViewCollection(ctx context.Context, coll *portaldomain.Collection, user *User) bool
CanViewCollection reports whether the user may view the collection (owner or any share).
func (*Checker) CanViewPrompt ¶
CanViewPrompt reports whether the user can see the prompt: global prompts are visible to all; personal prompts to their owner, admins, or share grantees.
func (*Checker) CollectionSharePermission ¶
func (c *Checker) CollectionSharePermission(ctx context.Context, collectionID string, user *User) portaldomain.SharePermission
CollectionSharePermission returns the highest share permission for a user on a collection. A store error, or no share store at all, yields the empty permission: a deployment without shares grants none rather than panicking on the write gates that now consult it.
func (*Checker) HasApplyKnowledge ¶
HasApplyKnowledge reports whether the user holds the apply_knowledge capability. It grants when the user's resolved persona lists the tool (the same Tools the frontend reads from GET /me and the MCP path gates on, so a non-admin persona granted apply_knowledge can review and promote), OR when the user is an admin.
Admins are always treated as holding the capability for two reasons: their persona normally grants every registered tool, and the tool may not be registered at all on a given deployment (apply_knowledge is absent when Knowledge.Apply.Enabled is false, its default), in which case the resolved Tools list can never contain it. Without the admin arm, enabling capability gating would lock admins out of knowledge writes wherever apply is disabled, a regression from the prior admin-role gate. The admin arm only widens access; the capability still grants non-admins, which is the behavior #661 requires.
func (*Checker) HasTool ¶
HasTool reports whether the user's resolved persona grants the named tool, or the user is an admin. It is the shared capability check behind the apply_knowledge and DataHub write authorizations; the admin arm only widens access (a separate write-enabled-connection check still applies to DataHub writes, so admin cannot mutate a read-only connection).
func (*Checker) OwnedAssetIDs ¶
OwnedAssetIDs filters ids down to the live assets the user owns.
func (*Checker) OwnedCollectionIDs ¶
OwnedCollectionIDs filters ids down to the live collections the user owns.
func (*Checker) OwnedTargetIDs ¶
func (c *Checker) OwnedTargetIDs(ctx context.Context, targetType string, ids []string, user *User) []string
OwnedTargetIDs filters ids down to the targets of the given type the user owns. An unsupported target type owns nothing.
func (*Checker) OwnsPersonalPrompt ¶
OwnsPersonalPrompt reports whether the user owns the given personal prompt.
func (*Checker) ResolveAssetPermission ¶
func (c *Checker) ResolveAssetPermission(ctx context.Context, assetID string, user *User) (portaldomain.SharePermission, error)
ResolveAssetPermission returns the highest permission a non-owner user holds for an asset, combining a direct share (AssetSharePermission) with a collection share (GetUserAssetPermissionViaCollection). A direct editor short-circuits the collection lookup because editor is the ceiling. The returned error is the direct-share store error (nil on success); a collection-lookup error is treated as no collection access, matching the best-effort cascade the view checks use. Owner access must be checked by the caller, which treats a non-nil error as 500.
This is for callers that need the effective permission value (getAsset reports it as SharePermission; the edit check compares it to editor). The bool view checks (CanViewAsset) deliberately do not use it: they only need "any grant" and short-circuit on a direct share to avoid a collection query per asset.
type Config ¶
type Config struct {
Assets portaldomain.AssetStore
Collections portaldomain.CollectionStore
Prompts prompt.Store
// AdminRoles are the roles that grant admin access in the portal.
AdminRoles []string
// PersonaTools resolves a user's roles to the tool names their persona
// grants. nil means no persona resolution is wired, in which case only
// the admin arm of the capability checks can grant.
PersonaTools func(roles []string) []string
}
Config carries the stores and identity settings the checks read. Every field is optional in the sense that a nil store disables the checks that need it (they deny rather than panic), which is what a portal deployed without collections or prompts requires.
type User ¶
type User struct {
UserID string
Email string
Roles []string
// FromCookie is true for browser-session (cookie) auth; only such requests
// are CSRF-enforced (API-key / Bearer auth is exempt).
FromCookie bool
// AuthType is HOW this caller was authenticated, as the authenticator that
// admitted them reported it. It is carried rather than re-derived because a
// surface that opens a session ON THIS CALLER'S BEHALF — the portal's
// script dry run (#1364) — must present the authentication the request
// actually arrived with, and FromCookie alone cannot tell an API key from a
// bearer token.
AuthType string
}
User holds information about the authenticated portal user.