authorization

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 13 Imported by: 0

README

authorization — 强授权档

YAML 操作目录(闭集校验 + 确定性哈希)+ 阶段化纯函数评估器 (authentication → scope → membership → role → policy → input,deny 携带阶段与 原因码)+ 三值决策(allow / deny / challenge)+ 规则注册表(内置 assurance / recent-auth 两条通用 step-up 规则,产品可注入自有规则与 fact 键)

  • 事务内 Facts Provider。

本包自包含:不 import 框架其他包(含 authz)。

authorization ↔ authz 概念映射(风险 F3 的解)

authz/(简单档)与本包(强档)并列共存,互不依赖:简单档是单层 RBAC 权限点检查,强档是操作目录驱动的闭集评估。不存在迁移压力——同一产品可以只用 简单档,也可以按操作逐个切换到强档。

什么场景用哪档
场景 用哪档
"角色 → 权限点"够用(单层 workspace,owner/admin/member 式角色映射) authz:Authorizer.Can(principal, "tasks:write"),一个中间件 Require 搞定路由保护
需要 step-up(AAL2 / recent-auth / sudo 模式) authorization:challenge 是一等决策,携带目标操作、要求的 assurance、max-age
凭据要绑 scope(细粒度 PAT / 机器凭据,GitHub FG-PAT 形态) authorization:ScopedTokenPrincipal / MachineCredentialPrincipal,scope 精确匹配不可替换
需要 deny 可解释(哪个阶段、什么原因码)供审计/反枚举策略 authorization:Decision{Stage, ReasonCode} 结构化拒绝
操作集要可哈希、可对账(目录 ↔ 路由 ↔ 契约比对) authorization:Catalog.Hash() 确定性哈希(对账伴侣包 contractx 已按 ADR 0012 删除——角色配置界面裁定为写入边界校验,不对账)
嵌套 scope(workspace → project 两层) 两档都留了形状;强档的 scope 输入类型(WorkspaceScope / ProjectScope)两层原生
词汇对应表
authz(简单档) authorization(强档) 对应关系
Permission(域:动作,如 tasks:write) Operation.OperationID(产品自定义,如 op.task.update) 权限点 ↔ 目录操作条目;强档每个操作是一条显式声明
域:* 通配、*_own 后缀 不存在(目录内禁通配;资源属主判定走产品规则注册表或调用方 facts) 强档闭集确定性优先;两者互斥(重做 §1.7)
RoleOwner/Admin/Member + RoleSet 显式集合 roles.workspace / roles.project 词汇表(产品数据)+ 操作的 allowed_*_roles 两档都无角色继承:owner ⊇ admin 靠显式集合,强档靠"交集非空"判定
Catalog(Go 代码 Register 权限点) Catalog(YAML 文件 + 严格 schema 校验 + 哈希) 简单档词汇在代码里;强档词汇是可对账的数据文件
Authorizer.Can/CanIn(ctx, p, perm) bool Evaluate(Request) Decision / Evaluator.Authorize(ctx, opID, ...) 两值布尔 ↔ 三值决策(含 challenge)
authz.Require(a, perm) 中间件 无中间件入口(刻意) F3 缓解:中间件入口不重复建设。强档消费者直接调 evaluator,传输层路由 resolver(method+path → operation)是产品侧职责
Provider.PermsFor + 30s TTL 缓存 FactsProvider.Facts(ctx, principal, scope),无缓存 一致性权衡差异:强档约定在调用方事务内取事实(撤销即时一致);接受读旧可自行包装 provider,那是显式选择
平台管理员旁路 IsPlatformAdmin → true 无旁路(每个操作显式声明,匿名操作用 auth_method: unauthenticated) 强档闭集哲学:没有隐式超级权限
—(无对应) Challenge / DenialError / ErrReauthenticationRequired step-up 通道:challenge 投影为 REAUTH 类稳定错误 + hint 载荷,不降级为权限类

快速上手

// 1. 写 operations.yaml(闭集:角色词汇 + 操作声明,无通配)
// 2. 启动期组装
registry := authorization.NewRegistry()          // 内置 assurance/recent-auth
registry.Register(myLifecycleRule{})             // 产品规则(可选)
registry.RegisterFact("project_lifecycle")       // 产品 fact 键(可选)
catalog, err := authorization.ParseCatalog(raw, registry)
evaluator, err := authorization.NewEvaluator(catalog, registry, myTxProvider)

// 3. 请求路径上(ctx 携带调用方事务)
decision, err := evaluator.Authorize(ctx, "op.task.update", principal,
    authorization.ProjectScope{WorkspaceID: "ws", ProjectID: "pj"}, time.Now())
switch decision.Outcome {
case authorization.OutcomeAllow:    // 放行
case authorization.OutcomeChallenge: // step-up:透出 decision.Challenge(目标操作/assurance/max-age)
    return decision.Err()            // errors.Is(err, ErrReauthenticationRequired)
default:                             // deny:阶段 + 原因码可审计
    return decision.Err()            // errors.Is(err, ErrDenied)
}

参考消费者(ADR 0005 重启,ADR 0013)

"逐操作切换到强档"的实例在 example/internal/authorization(ADR 0013 batch 1-3):

  • catalog.yaml —— 产品操作目录(workspace.delete / workspace.transfer_ownership / token.create),闭集 + 逐操作显式 recent-auth 窗口;
  • resolver.go —— method+path → operationID(组合根职责,arch A6;第二个消费者 (runlynk)确认形状后才考虑提升为框架包),启动期与目录双向绑定(Bind)防漂移;
  • facts.go —— 事务内 FactsProvider 参考实现(读调用方自己的 pgx.Tx,TOCTOU 回归 钉:同事务撤销成员资格 → 同事务决策翻转为 deny);
  • middleware.go —— 决策投影:allow 放行;deny → 403 PERMISSION_DENIED (stage+reason);challenge → 403 E_REAUTH_REQUIRED(details 携带 target_operation / required_assurance / max_age_seconds / factor),粘到 webx 重认证流程 (POST /auth/confirm-password 或 POST /auth/verify-mfa)后重试。

强档接线四步(照抄 example 的形状)

  1. resolver:路由形状 → 操作 ID;未登记形状一律拒绝(fail-closed)。启动时 Bind:目录里每个操作必须有路由、每条路由必须指向目录操作,漂移在启动期爆;
  2. transport 匹配:一次分类(webx.AuthConfig.ClassifyTransport,与 Authenticate 同源)对照操作声明的 auth_method(映射就五行,留在组合根); mixed(cookie + bearer 同时出现)→ 401 E_AUTH_TRANSPORT_CONFLICT,永不 按优先级择一解析(GitLab #22435 教训);
  3. 评估:FactsProvider 在调用方事务内取事实;challenge 路径先做 terminal 复核(强制新鲜重评估,membership/role 仍然 deny 则最终答案 deny —— ADR 0011 组合次序的计算形态);
  4. 投影:见上;403 永远携带 stage+reason(可审计),challenge 永远携带 target hint(可恢复)。

护栏(明确不做)

  • 目录内禁 域:* 通配(与简单档互斥);
  • 无角色继承魔法(显式集合 / 交集非空);
  • 只做两层 scope 输入形状(workspace → project),不做任意深度;
  • 不做 ReBAC / 策略引擎;不内置 TTL 缓存;
  • 审批编排、Phase0、具名审批不进(组织纪律留产品)。

与 webx step-up 的关系(ADR 0011 / 0013)

step-up 有两条路,按策略形态选一条,同一操作禁止双挂载(会双重挑战,第二次 挑战在第一次完成后立即触发,用户永远过不去)。禁令覆盖三个坐标轴——静态 vs 计算、 password vs MFA、以及本包 vs webx.RequireRecentMFA:

  • 静态路由级("删除工作区要最近 15 分钟内输过密码")→ webx.RequireRecentAuth(maxAge) / webx.RequireRecentMFA(maxAge)(第二因子孪生,ADR 0013 batch 2)+ POST /auth/confirm-password / POST /auth/verify-mfa,机制长在认证层,与业界 (Laravel password.confirm / GitHub sudo / Clerk Reverification)同构;不需要本包。
  • 计算型(目录/规则驱动:"只对生产资源要求""改动他人数据时才要求")→ 本包 Challenge 决策:内置 assurance / recent-auth 规则经 FactsProvider 读 session 的 password_confirmed_at / mfa_confirmed_at(与 webx 盖的是同两列,ADR 0011 数据源规则),投影为 ErrReauthenticationRequired 后由产品粘到 webx 的重认证 流程再重试。

窗口长短是产品策略(GitHub 2h / Laravel 3h / example 15min):webx 侧逐挂载点 显式传 maxAge;本包侧逐操作声明 recent_auth_window_seconds(必填、无包级 默认——原 600s 包级常量已随 ADR 0013 batch 1 删除)。

组合次序(两条路共用,测试钉死):权限检查最外层(角色拒绝是终态),step-up 挑战在内层(可恢复)——无权成员永远不该被"哄着"去重认证。计算形态下由 challenge 路径的 terminal 复核保证(example/middleware.go 的 forcedFreshFacts 重评估)。

机器主体语义

PAT / 机器凭据在 challenge 面前永远拿普通 403,绝不是 challenge:机器无法 交互式重认证,作用域凭据不得触及"留给在场的真人"的操作(webx 中间件与强档 投影一致;测试钉死在 example 与 webx 两侧)。

golden 基准

evaluator_golden_test.go 锁定评估器的确定性语义(阶段顺序/原因码/challenge 投影) (docs/ploykit-m0-golden-subset.md,13 例:G-1..G-7 两值 + CH-1..CH-6 挑战, CH-6 为 ADR 0013 batch 1 的逐操作窗口参数化钉),全部在"空产品注册表 + 仅内置 规则"下通过——空注册表可过 = 通用。

Documentation

Index

Constants

View Source
const (
	ReasonAllowed                      = "allowed"
	ReasonUnknownOperationOrScope      = "unknown_operation_or_scope"
	ReasonAuthMethodNotAllowed         = "auth_method_not_allowed"
	ReasonPrincipalRequired            = "principal_required"
	ReasonSessionInactive              = "session_inactive"
	ReasonCredentialInactive           = "credential_inactive"
	ReasonAssuranceRequired            = "assurance_required"
	ReasonRecentAuthRequired           = "recent_auth_required"
	ReasonScopeMismatch                = "scope_mismatch"
	ReasonPrincipalScopeMismatch       = "principal_scope_mismatch"
	ReasonCredentialMembershipInactive = "credential_membership_inactive"
	ReasonWorkspaceMembershipInactive  = "workspace_membership_inactive"
	ReasonProjectMembershipInactive    = "project_membership_inactive"
	ReasonWorkspaceRoleNotAllowed      = "workspace_role_not_allowed"
	ReasonProjectRoleNotAllowed        = "project_role_not_allowed"
	ReasonIdempotencyInvalid           = "idempotency_invalid"
)
View Source
const (
	DenialCodePermissionDenied = "PERMISSION_DENIED"
	DenialCodeReauthRequired   = "REAUTH_REQUIRED"
)
View Source
const (
	RuleNameAssurance  = "assurance"
	RuleNameRecentAuth = "recent_auth"
)
View Source
const CurrentCatalogSchemaVersion = 1

Variables

View Source
var (
	ErrDenied = errors.New("authorization: denied")

	ErrReauthenticationRequired = errors.New("authorization: reauthentication required")
)

Functions

This section is empty.

Types

type Assurance

type Assurance string
const (
	AssuranceNone Assurance = "none"
	AssuranceAAL1 Assurance = "aal1"
	AssuranceAAL2 Assurance = "aal2"
)

type AuthMethod

type AuthMethod string
const (
	AuthMethodUnauthenticated   AuthMethod = "unauthenticated"
	AuthMethodSession           AuthMethod = "session"
	AuthMethodScopedToken       AuthMethod = "scoped_token"
	AuthMethodMachineCredential AuthMethod = "machine_credential"
)

type Catalog

type Catalog struct {
	CatalogSchemaVersion int         `yaml:"catalog_schema_version" json:"catalog_schema_version"`
	Roles                RoleVocab   `yaml:"roles" json:"roles"`
	Operations           []Operation `yaml:"operations" json:"operations"`
}

func ParseCatalog

func ParseCatalog(raw []byte, reg *Registry) (Catalog, error)

func (Catalog) Hash

func (c Catalog) Hash() (string, error)

func (Catalog) Operation

func (c Catalog) Operation(operationID string) (Operation, bool)

func (Catalog) Validate

func (c Catalog) Validate(reg *Registry) error

type Challenge

type Challenge struct {
	TargetOperationID string
	RequiredAssurance Assurance
	MaxAge            time.Duration
}

type Decision

type Decision struct {
	Outcome    Outcome
	Stage      Stage
	ReasonCode string
	Challenge  *Challenge
}

func Evaluate

func Evaluate(request Request) Decision

func NewChallenge

func NewChallenge(op Operation) Decision

func (Decision) Allowed

func (d Decision) Allowed() bool

func (Decision) Err

func (d Decision) Err() error

type DenialError

type DenialError struct {
	Decision   Decision
	Code       string
	ReauthHint *Challenge
}

func (*DenialError) Error

func (e *DenialError) Error() string

func (*DenialError) Is

func (e *DenialError) Is(target error) bool

type Evaluator

type Evaluator struct {
	// contains filtered or unexported fields
}

func NewEvaluator

func NewEvaluator(catalog Catalog, registry *Registry, provider FactsProvider) (*Evaluator, error)

func (*Evaluator) Authorize

func (e *Evaluator) Authorize(ctx context.Context, operationID string, principal Principal, scope Scope, now time.Time) (Decision, error)

func (*Evaluator) Catalog

func (e *Evaluator) Catalog() Catalog

func (*Evaluator) Registry

func (e *Evaluator) Registry() *Registry

type Facts

type Facts struct {
	ActiveUser            bool
	ActiveSession         bool
	CredentialGrantActive bool

	ActiveWorkspaceMembership bool
	ActiveProjectMembership   bool

	WorkspaceRoles []string
	ProjectRoles   []string

	Assurance        Assurance
	RecentPasswordAt time.Time
	RecentMFAAt      time.Time

	IdempotencyValid bool

	Product map[string]any
}

type FactsProvider

type FactsProvider interface {
	Facts(ctx context.Context, principal Principal, scope Scope) (Facts, error)
}

type IdempotencyRule

type IdempotencyRule string
const (
	IdempotencyNone     IdempotencyRule = "none"
	IdempotencyRequired IdempotencyRule = "required"
)

type MachineCredentialPrincipal

type MachineCredentialPrincipal struct {
	CredentialID string
	Scope        ProjectScope
	NotBefore    time.Time
	ExpiresAt    time.Time
}

func (MachineCredentialPrincipal) AuthMethod

func (MachineCredentialPrincipal) Subject

func (p MachineCredentialPrincipal) Subject() string

type Operation

type Operation struct {
	OperationID           string         `yaml:"operation_id" json:"operation_id"`
	AuthMethod            AuthMethod     `yaml:"auth_method" json:"auth_method"`
	Scope                 ScopeKind      `yaml:"scope" json:"scope"`
	AllowedWorkspaceRoles []string       `yaml:"allowed_workspace_roles" json:"allowed_workspace_roles,omitempty"`
	AllowedProjectRoles   []string       `yaml:"allowed_project_roles" json:"allowed_project_roles,omitempty"`
	Assurance             Assurance      `yaml:"assurance" json:"assurance"`
	RecentAuth            RecentAuthRule `yaml:"recent_auth" json:"recent_auth"`
	// RecentAuthWindowSeconds is the step-up window for recent-auth operations,
	// declared explicitly per operation — there is deliberately no package
	// default (ADR 0011: the window is product policy; the hardcoded 600s
	// package constant was removed with this field). Required (positive) when
	// recent_auth is not none; forbidden (zero) when it is.
	RecentAuthWindowSeconds int             `yaml:"recent_auth_window_seconds" json:"recent_auth_window_seconds,omitempty"`
	Idempotency             IdempotencyRule `yaml:"idempotency" json:"idempotency"`

	Rules []string `yaml:"rules" json:"rules,omitempty"`
}

func (Operation) RecentAuthWindow added in v0.5.0

func (op Operation) RecentAuthWindow() time.Duration

RecentAuthWindow converts the declared window to a duration. Only meaningful for operations with recent_auth != none (validation guarantees a positive value there).

type Outcome

type Outcome uint8
const (
	OutcomeDeny Outcome = iota
	OutcomeAllow
	OutcomeChallenge
)

type PlatformScope

type PlatformScope struct{}

func (PlatformScope) Project

func (PlatformScope) Project() (string, bool)

func (PlatformScope) Workspace

func (PlatformScope) Workspace() (string, bool)

type Principal

type Principal interface {
	AuthMethod() AuthMethod

	Subject() string
	// contains filtered or unexported methods
}

type ProjectScope

type ProjectScope struct {
	WorkspaceID string
	ProjectID   string
}

func (ProjectScope) Project

func (s ProjectScope) Project() (string, bool)

func (ProjectScope) Workspace

func (s ProjectScope) Workspace() (string, bool)

type RecentAuthRule

type RecentAuthRule string
const (
	RecentAuthNone           RecentAuthRule = "none"
	RecentAuthPassword       RecentAuthRule = "password"
	RecentAuthPasswordAndMFA RecentAuthRule = "password_and_mfa"
)

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

func NewRegistry

func NewRegistry() *Registry

func (*Registry) FactNames

func (r *Registry) FactNames() []string

func (*Registry) IsBuiltin

func (r *Registry) IsBuiltin(name string) bool

func (*Registry) Register

func (r *Registry) Register(rule Rule) error

func (*Registry) RegisterFact

func (r *Registry) RegisterFact(name string) error

func (*Registry) Resolve

func (r *Registry) Resolve(names []string) ([]Rule, error)

func (*Registry) Rule

func (r *Registry) Rule(name string) (Rule, bool)

func (*Registry) RuleNames

func (r *Registry) RuleNames() []string

func (*Registry) ValidateFacts

func (r *Registry) ValidateFacts(facts Facts) error

type Request

type Request struct {
	Operation Operation

	Principal Principal
	Scope     Scope
	Facts     Facts

	Now time.Time

	Rules []Rule
}

type RoleVocab

type RoleVocab struct {
	Workspace []string `yaml:"workspace" json:"workspace"`
	Project   []string `yaml:"project" json:"project"`
}

type Rule

type Rule interface {
	RuleName() string

	Stage() Stage

	Evaluate(Request) (Decision, bool)
}

type Scope

type Scope interface {
	Workspace() (string, bool)

	Project() (string, bool)
	// contains filtered or unexported methods
}

type ScopeKind

type ScopeKind string
const (
	ScopeKindPlatform ScopeKind = "platform"

	ScopeKindWorkspacePath ScopeKind = "workspace_path"

	ScopeKindProjectPath ScopeKind = "project_path"

	ScopeKindProjectGovernance ScopeKind = "project_governance"

	ScopeKindProjectCredential ScopeKind = "project_credential"
)

type ScopedTokenPrincipal

type ScopedTokenPrincipal struct {
	SubjectID string
	TokenID   string
	Scope     ProjectScope
}

func (ScopedTokenPrincipal) AuthMethod

func (ScopedTokenPrincipal) AuthMethod() AuthMethod

func (ScopedTokenPrincipal) Subject

func (p ScopedTokenPrincipal) Subject() string

type SessionPrincipal

type SessionPrincipal struct {
	SubjectID      string
	SessionID      string
	Assurance      Assurance
	AbsoluteExpiry time.Time
	IdleExpiry     time.Time
}

func (SessionPrincipal) AuthMethod

func (SessionPrincipal) AuthMethod() AuthMethod

func (SessionPrincipal) Subject

func (p SessionPrincipal) Subject() string

type Stage

type Stage string
const (
	StageAuthentication Stage = "authentication"
	StageScope          Stage = "scope"
	StageMembership     Stage = "membership"
	StageRole           Stage = "role"
	StagePolicy         Stage = "policy"
	StageInput          Stage = "input"

	StageComplete Stage = "complete"
)

type WorkspaceScope

type WorkspaceScope struct{ WorkspaceID string }

func (WorkspaceScope) Project

func (WorkspaceScope) Project() (string, bool)

func (WorkspaceScope) Workspace

func (s WorkspaceScope) Workspace() (string, bool)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL