projectmember

package
v0.0.7 Latest Latest
Warning

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

Go to latest
Published: Oct 27, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package projectmember is the durable per-project member list: who besides a project's owner may reach it, and at what role (Task 20366).

It is the split pkg/rolestore and pkg/sessionstore already use: pkg/authz owns the role model and stays stdlib-only, pkg/statedb owns the table, and this package owns normalisation and the cache that makes reading the list on every authorization decision affordable.

What a membership is

An additive grant scoped to one project: "this identity is also admitted to this path, at this role". The hub unions it with whatever the identity already holds (authz.Union), so sharing a project can never demote anybody. It is also never more than the granter's own authority on that project — the REST layer enforces that cap, because this package does not know who is asking.

Freshness

Reads are served from an in-memory snapshot refreshed every TTL, so a write another process made (the CLI, another hub member) lands within one TTL. Writes through a Store land at once: the Store reloads — or, should the database refuse the read, patches its snapshot with the change it just committed — before returning, so "I removed them" holds on the very next request. Invalidate makes the next read reload; pkg/ui calls it when another hub member announces a change on the cluster bus. Every reload that finds the list changed is reported to the OnChange callback, which is how a hub closes the live streams of somebody who just lost access.

Failure policy

A refresh that fails keeps serving the last good snapshot. The table only grants, so an empty answer would be the "safe" direction, but it would also revoke every collaborator for as long as a storage blip lasted. The first load happens in New and its failure is fatal to hub startup, so the degraded case is bounded by a snapshot that was known-good once. A row whose role this build cannot parse is skipped and reported, never fatal: one unreadable row must not un-share every project on the hub.

Index

Constants

View Source
const DefaultTTL = 10 * time.Second

DefaultTTL bounds how long a membership written by another process takes to reach this one's authorization decisions.

View Source
const MaxKeyLen = 320

MaxKeyLen bounds an identity key. RFC 5321 caps an address at 254 octets; the rest is room for a "sub:" prefix on a long opaque subject.

View Source
const MaxReasonLen = 500

MaxReasonLen bounds the free-text reason stored with a membership.

Variables

This section is empty.

Functions

func KeyFor

func KeyFor(email, sub string) string

KeyFor derives the membership key for an identity's claims, exactly as oidcauth.Identity.OwnerKey does: the lowercased email, or "sub:" and the subject when the identity provider released no email. "" when it has neither. One definition, because the key is the namespace project owners and members share, and a second spelling would match nobody.

func NormalizeKey

func NormalizeKey(key string) string

NormalizeKey canonicalises an identity key without judging it: emails are lowercased, because an identity provider may release a casing the operator did not type; a "sub:" key keeps its subject's case, because a subject is an opaque identifier and two differing only in case are two people.

func NormalizePath

func NormalizePath(path string) string

NormalizePath canonicalises a project path so two spellings of one directory are one membership: absolute and cleaned, but deliberately not symlink-resolved, because the registry and authz.Scope.ProjectPath carry the path as registered and a resolved key would match neither.

func ParseGrantableRole

func ParseGrantableRole(raw string) (authz.Role, error)

ParseGrantableRole parses a role a membership may carry: any rung of the ladder above "none". A membership granting nothing is a revocation in a membership's clothes, and would list somebody on a project they cannot open.

func ParseKey

func ParseKey(raw string) (string, error)

ParseKey normalises a key typed by an operator and refuses one that could never match a signed-in identity: OwnerKey is always an email or "sub:" and a subject, so anything else would be stored, listed, and grant nobody anything.

Types

type Audit

type Audit func(prev, next *Member) ([]*statedb.AuditEvent, error)

Audit builds the audit events committed together with a change; prev and next are nil when there was no membership before or after. See statedb.MemberAudit.

type Change

type Change struct {
	// Projects are the paths whose roster changed.
	Projects []string
	// Identities are the keys whose memberships changed.
	Identities []string
}

Change is what a reload found different from the snapshot before it.

func (Change) Empty

func (c Change) Empty() bool

Empty reports whether nothing changed.

type Member

type Member struct {
	// ProjectPath is the project's normalised absolute path.
	ProjectPath string `json:"project_path"`
	// IdentityKey is the grantee in oidcauth.Identity.OwnerKey's namespace.
	IdentityKey string `json:"identity"`
	// Role is what the grantee may do on the project.
	Role authz.Role `json:"role"`
	// Reason is why they were admitted; may be empty.
	Reason string `json:"reason,omitempty"`
	// GrantedAt and GrantedBy record the most recent write.
	GrantedAt time.Time `json:"granted_at"`
	GrantedBy string    `json:"granted_by,omitempty"`
}

Member is one membership as the rest of the hub sees it.

type Option

type Option func(*Store)

Option configures a Store.

func WithClock

func WithClock(now func() time.Time) Option

WithClock injects a clock, for tests.

func WithErrorHandler

func WithErrorHandler(fn func(error)) Option

WithErrorHandler installs a sink for failed refreshes and skipped rows.

func WithOnChange

func WithOnChange(fn func(Change)) Option

WithOnChange installs the callback a reload calls, on its own goroutine, when the list changed.

func WithTTL

func WithTTL(d time.Duration) Option

WithTTL overrides the refresh interval. A non-positive value is ignored: a zero TTL would put a query on every permission check of every request.

type Store

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

Store reads memberships through a TTL cache.

func New

func New(db *statedb.DB, opts ...Option) (*Store, error)

New adapts a control-plane database and performs the first load.

The database must be the hub's own, never a managed project's: a tenant able to write the file holding this table could admit itself to every project on the hub with one INSERT.

func (*Store) All

func (s *Store) All() []Member

All returns every membership, ordered by project then identity.

func (*Store) Any

func (s *Store) Any() bool

Any reports whether any project on the hub has been shared. Served from the cache, so a hub that never shared anything pays one slice length per check.

func (*Store) Grant

func (s *Store) Grant(m Member, audit Audit) (Member, *Member, error)

Grant records or replaces a membership and returns it as stored, along with the membership it replaced (nil for a new member). audit builds the rows committed with it. The grant is in force for this process when Grant returns.

func (*Store) Invalidate

func (s *Store) Invalidate()

Invalidate makes the next read reload from the database.

func (*Store) Lookup

func (s *Store) Lookup(projectPath, identityKey string) (Member, bool)

Lookup returns identityKey's membership of projectPath.

func (*Store) MembersOf

func (s *Store) MembersOf(projectPath string) []Member

MembersOf returns projectPath's members ordered by identity.

func (*Store) ProjectsOf

func (s *Store) ProjectsOf(identityKey string) []Member

ProjectsOf returns identityKey's memberships ordered by project.

func (*Store) Refresh

func (s *Store) Refresh() error

Refresh reloads now, whatever the TTL says, and reports a failure. The change it finds, if any, goes to the OnChange callback.

func (*Store) Revoke

func (s *Store) Revoke(projectPath, identityKey string, audit Audit) (*Member, error)

Revoke removes a membership and returns it, or nil when there was none. The removal is in force for this process when Revoke returns.

func (*Store) RevokeProject

func (s *Store) RevokeProject(projectPath string, audit Audit) ([]Member, error)

RevokeProject removes every membership on a project that is leaving the hub.

func (*Store) RoleFor

func (s *Store) RoleFor(projectPath, identityKey string) (authz.Role, bool)

RoleFor returns the role identityKey holds on projectPath by membership.

Jump to

Keyboard shortcuts

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