159 lines
5.7 KiB
Go
159 lines
5.7 KiB
Go
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
|
|
}
|