internal/accessauth/accessauth.go
Rama main · réplica de lectura del repositorio original.
// Package accessauth is Sparrow's adapter for pkg/access: it fixes the
// secret prefixes, maps the realm to the request's tenant and the scope to a
// consumer, and holds Sparrow's lifetime rules for tokens and invites.
//
// Everything generic (issuing, hashing, redeeming, caching) lives in
// pkg/access; nothing here should be needed by another application.
package accessauth
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"unicode"
"github.com/sarathsp06/sparrow/internal/crypto"
"github.com/sarathsp06/sparrow/internal/tenant"
"github.com/sarathsp06/sparrow/pkg/access"
"github.com/sarathsp06/sparrow/pkg/access/pgstore"
)
// Secret prefixes. Distinctive so leaked secrets are easy to spot and scan for.
const (
TokenPrefix = "sparrow_tk_"
InvitePrefix = "sparrow_inv_"
)
// MasterKeyName is how SPARROW_API_KEY appears as a principal and in created_by.
const MasterKeyName = "master key"
// Realm is the realm of the tenant ctx runs as (tenant.FromContext): tokens
// and invites are created in it, listed and revoked from it, and accepted only
// on requests for it. A self-hosted server has one tenant, the default one.
func Realm(ctx context.Context) string { return tenant.FromContext(ctx).String() }
// RootRealm is the realm of SPARROW_API_KEY. The master key is the server's,
// not a tenant's, so it is accepted for every tenant (see middleware.Auth).
func RootRealm() string { return tenant.DefaultTenantID.String() }
// Lifetime rules.
// - Tenant-wide tokens act exactly like SPARROW_API_KEY. They expire after
// the operator's default (SPARROW_TOKEN_DEFAULT_TTL, 90 days unless
// changed) unless a TTL is given, or never when explicitly asked for.
// - Consumer tokens reach outside users, so they always expire.
// - Invites are links in chat: short-lived by default.
const (
TenantTokenDefaultTTL = 90 * 24 * time.Hour
ConsumerTokenDefaultTTL = 7 * 24 * time.Hour
ConsumerTokenMaxTTL = 30 * 24 * time.Hour
InviteDefaultTTL = 24 * time.Hour
InviteMaxTTL = 7 * 24 * time.Hour
// TokenRetention is how long expired and revoked tokens stay listed
// before the daily purge deletes them.
TokenRetention = 7 * 24 * time.Hour
)
// TokenTTL applies the lifetime rules to a requested TTL (0 = default).
// neverExpires asks for a tenant-wide token without expiry; tenantDefault is
// the lifetime of a tenant-wide token when neither is given (0 = never).
// A returned 0 means the token never expires.
func TokenTTL(consumer *string, requested time.Duration, neverExpires bool, tenantDefault time.Duration) (time.Duration, error) {
if neverExpires && requested > 0 {
return 0, fmt.Errorf("set either a TTL or never_expires, not both")
}
if consumer == nil {
switch {
case neverExpires:
return 0, nil
case requested > 0:
return requested, nil
default:
return tenantDefault, nil
}
}
if neverExpires {
return 0, fmt.Errorf("consumer tokens always expire (at most %d days)", ConsumerTokenMaxTTL/(24*time.Hour))
}
if requested == 0 {
return ConsumerTokenDefaultTTL, nil
}
if requested > ConsumerTokenMaxTTL {
return 0, fmt.Errorf("consumer tokens can live at most %d days", ConsumerTokenMaxTTL/(24*time.Hour))
}
return requested, nil
}
// InviteTTL applies the lifetime rules to a requested invite TTL (0 = default).
func InviteTTL(requested time.Duration) (time.Duration, error) {
if requested == 0 {
return InviteDefaultTTL, nil
}
if requested > InviteMaxTTL {
return 0, fmt.Errorf("invites can live at most %d days", InviteMaxTTL/(24*time.Hour))
}
return requested, nil
}
// ValidateConsumer checks a consumer name for a consumer-scoped token or
// invite. Consumers are addressed as one URL path segment
// (/v1/consumers/{consumer}/...), which the portal gateway builds from the
// token's scope, so a scope must be a single, literal segment. The reserved
// system consumer is refused: only tenant-wide credentials may reach it.
func ValidateConsumer(consumer string) error {
switch {
case consumer == tenant.SystemConsumer:
return fmt.Errorf("consumer %q is reserved for Sparrow's system events and needs a tenant-wide credential", consumer)
case strings.TrimSpace(consumer) != consumer || consumer == "":
return errors.New("consumer must be non-empty with no leading or trailing spaces")
case len(consumer) > 255:
return errors.New("consumer is longer than 255 characters")
case consumer == "." || consumer == ".." || strings.ContainsAny(consumer, "/\\?#%"):
return fmt.Errorf("consumer %q is not a valid name (no /, \\, ?, #, %%, or dot segments)", consumer)
}
for _, r := range consumer {
if unicode.IsControl(r) {
return errors.New("consumer must not contain control characters")
}
}
return nil
}
// New builds the access service over Postgres. apiKey (SPARROW_API_KEY) is
// registered as the root key when set; sealer (see Sealer) stores the secrets
// of tokens with an external id and may be nil to disable them.
func New(db *sql.DB, apiKey string, sealer access.SecretSealer) (*access.Service, error) {
return NewWithStore(pgstore.New(db), apiKey, sealer)
}
// NewWithStore is New with any store (tests use memstore).
func NewWithStore(store access.Store, apiKey string, sealer access.SecretSealer) (*access.Service, error) {
var roots []access.RootKey
if apiKey != "" {
roots = []access.RootKey{{Secret: apiKey, Realm: RootRealm(), Name: MasterKeyName}}
}
return access.New(access.Config{Store: store, RootKeys: roots, Sealer: sealer, TokenPrefix: TokenPrefix, InvitePrefix: InvitePrefix})
}
// Sealer seals the stored secrets of tokens with an external id with Sparrow's
// envelope encryption (the SPARROW_ENCRYPTION_KEYS keyring), like every other
// secret at rest.
func Sealer(svc *crypto.Service) access.SecretSealer { return cryptoSealer{svc} }
type cryptoSealer struct{ svc *crypto.Service }
func (c cryptoSealer) Seal(p []byte) ([]byte, error) { return c.svc.EnvelopeEncrypt(p) }
func (c cryptoSealer) Open(s []byte) ([]byte, error) { return c.svc.EnvelopeDecrypt(s) }