diff --git a/auth/auth.go b/auth/auth.go index d8d47a4..a7f11fc 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -36,6 +36,7 @@ var ErrNoCredentials = errors.New("auth: no credentials") type Principal struct { Subject string `json:"subject"` // OIDC sub or API key id Scopes []string `json:"scopes,omitempty"` // e.g. ["write","admin"] + Roles []string `json:"roles,omitempty"` // e.g. ["editor","publisher","admin"] Kind string `json:"kind"` // KindOIDC | KindAPIKey | KindAnonymous } @@ -44,6 +45,32 @@ func (p *Principal) HasScope(scope string) bool { return slices.Contains(p.Scopes, scope) } +// HasRole reports whether the principal carries the given role (or is an admin). +func (p *Principal) HasRole(role string) bool { + if role == "" || role == "*" { + return true + } + for _, r := range p.Roles { + if r == "admin" || r == role { + return true + } + } + return false +} + +// HasAnyRole reports whether the principal carries at least one of the given roles. +func (p *Principal) HasAnyRole(roles ...string) bool { + if len(roles) == 0 { + return true + } + for _, r := range roles { + if p.HasRole(r) { + return true + } + } + return false +} + // Authenticator inspects request credentials and returns the caller, or an // error if authentication fails. type Authenticator interface { diff --git a/auth/oidc.go b/auth/oidc.go index 251c610..a72b31d 100644 --- a/auth/oidc.go +++ b/auth/oidc.go @@ -67,7 +67,7 @@ func (o *OIDC) Authenticate(r *http.Request) (*Principal, error) { if err != nil { return nil, err } - return &Principal{Subject: c.Subject, Scopes: c.Scopes, Kind: KindOIDC}, nil + return &Principal{Subject: c.Subject, Scopes: c.Scopes, Roles: c.Roles, Kind: KindOIDC}, nil } // Claims is the parsed, verified claim set returned by VerifyClaims. Only @@ -83,6 +83,7 @@ type Claims struct { Expires int64 `json:"exp"` NotBefore int64 `json:"nbf"` Scopes []string `json:"scope,omitempty"` // parsed from "scope" space-separated claim + Roles []string `json:"roles,omitempty"` // parsed from "roles" or "groups" claim } // Audience unmarshals the `aud` claim, which RFC 7519 allows to be either a @@ -187,6 +188,8 @@ func (o *OIDC) VerifyClaims(token string) (*Claims, error) { Expires int64 `json:"exp"` NotBefore int64 `json:"nbf"` Scope string `json:"scope"` + Roles []string `json:"roles"` + Groups []string `json:"groups"` } if err := json.Unmarshal(payloadJSON, &raw); err != nil { return nil, fmt.Errorf("oidc: bad claims: %w", err) @@ -209,6 +212,11 @@ func (o *OIDC) VerifyClaims(token string) (*Claims, error) { if raw.Scope != "" { scopes = strings.Fields(raw.Scope) } + roles := raw.Roles + if len(roles) == 0 && len(raw.Groups) > 0 { + roles = raw.Groups + } + return &Claims{ Issuer: raw.Issuer, Subject: raw.Subject, @@ -218,6 +226,7 @@ func (o *OIDC) VerifyClaims(token string) (*Claims, error) { Expires: raw.Expires, NotBefore: raw.NotBefore, Scopes: scopes, + Roles: roles, }, nil }