package extension import ( "context" "errors" "net/http" ) const CapabilityExplainSchemaVersion = 1 // Capability sources name where an allowed action comes from. const ( CapabilitySourceRole = "role" CapabilitySourceSystemGrant = "system-grant" CapabilitySourceBootstrapAdmin = "bootstrap-admin" ) // Capability scopes name where an allowed action applies. const ( CapabilityScopeSystem = "system" CapabilityScopeOwnProjects = "own-projects" CapabilityScopeMemberProjects = "member-projects" ) // CapabilitySubject identifies the Host-bound actor and the authority snapshot // an explanation was computed from. It never names another account. type CapabilitySubject struct { SchemaVersion int `json:"schemaVersion"` UserID string `json:"userId"` Owner string `json:"owner"` AuthorityEpoch int64 `json:"authorityEpoch"` RoleID string `json:"roleId,omitempty"` RoleName string `json:"roleName,omitempty"` RoleEnabled bool `json:"roleEnabled"` } // CapabilityActionExplain reports one action's effective decision. Source is // set only when Allowed; LinkedVia names the action that links it; Conflict // marks a linked action that the enabled role caps off. type CapabilityActionExplain struct { ActionID string `json:"actionId"` Scope string `json:"scope"` Allowed bool `json:"allowed"` CapAllowed bool `json:"capAllowed"` Source string `json:"source,omitempty"` LinkedVia string `json:"linkedVia,omitempty"` Conflict bool `json:"conflict,omitempty"` Reason string `json:"reason,omitempty"` } // CapabilityExplain is the creator explanation: project.create, the linked // own-project management actions, their conflicts and the migration note. type CapabilityExplain struct { CapabilitySubject ProjectCreate CapabilityActionExplain `json:"projectCreate"` Linked []CapabilityActionExplain `json:"linked"` Conflicts []string `json:"conflicts"` MigrationNote string `json:"migrationNote,omitempty"` } // MemberBasicCollaborate explains the role defaults a member brings to the // projects they are a member of. Bootstrap admin is not a content source. type MemberBasicCollaborate struct { CapabilitySubject Actions []CapabilityActionExplain `json:"actions"` } // DocumentDutyQualify reports whether the role cap lets a document duty be // assigned to the actor and whether the role grants it by default. type DocumentDutyQualify struct { ActionID string `json:"actionId"` Qualified bool `json:"qualified"` DefaultAllowed bool `json:"defaultAllowed"` Source string `json:"source,omitempty"` Reason string `json:"reason,omitempty"` } type DocumentQualify struct { CapabilitySubject Duties []DocumentDutyQualify `json:"duties"` } // CapabilityExplainProvider is optional on a trusted dispatch Host. Every // method explains only the Host-bound actor; callers cannot name another one. type CapabilityExplainProvider interface { CurrentCapabilityExplain(context.Context) (CapabilityExplain, error) CurrentMemberBasicCollaborate(context.Context) (MemberBasicCollaborate, error) CurrentDocumentQualify(context.Context) (DocumentQualify, error) } // Work operations whose failures are categorized for clients. const ( WorkOperationProjectCreate = "project.create" WorkOperationMemberAdd = "project.member.add" WorkOperationConversation = "conversation.work" ) // Work failure reasons. Each maps to exactly one HTTP status. const ( WorkFailureDuplicateName = "duplicate-name" WorkFailurePolicyRejected = "policy-rejected" WorkFailureMappingNotReady = "mapping-not-ready" WorkFailureServiceUnavailable = "service-unavailable" ) // WorkFailure is a categorized refusal a provider may return from project // create, member add or conversation work. Its text carries only the operation // and reason, never an account, project or name. type WorkFailure struct { Operation string `json:"operation"` Reason string `json:"reason"` } func (f WorkFailure) Error() string { return f.Operation + ": " + f.Reason } // StatusCode returns 0 for an unknown reason so it is never trusted as a status. func (f WorkFailure) StatusCode() int { switch f.Reason { case WorkFailureDuplicateName: return http.StatusConflict case WorkFailurePolicyRejected: return http.StatusForbidden case WorkFailureMappingNotReady, WorkFailureServiceUnavailable: return http.StatusServiceUnavailable } return 0 } // CategorizeWorkFailure classifies a provider error for operation. A known // WorkFailure keeps its reason; a coded 403/503 (and 409 outside conversation // work) maps to its category; a deadline is service-unavailable. Anything else // is left uncategorized so the caller keeps its existing not-found handling. func CategorizeWorkFailure(operation string, err error) (WorkFailure, bool) { if err == nil { return WorkFailure{}, false } var failure WorkFailure if errors.As(err, &failure) && failure.StatusCode() != 0 { return WorkFailure{Operation: operation, Reason: failure.Reason}, true } var coded interface{ StatusCode() int } if errors.As(err, &coded) { switch coded.StatusCode() { case http.StatusForbidden: return WorkFailure{Operation: operation, Reason: WorkFailurePolicyRejected}, true case http.StatusConflict: if operation != WorkOperationConversation { return WorkFailure{Operation: operation, Reason: WorkFailureDuplicateName}, true } case http.StatusServiceUnavailable: return WorkFailure{Operation: operation, Reason: WorkFailureServiceUnavailable}, true } } if errors.Is(err, context.DeadlineExceeded) { return WorkFailure{Operation: operation, Reason: WorkFailureServiceUnavailable}, true } return WorkFailure{}, false }