Documentation
¶
Overview ¶
Package orgsso owns the per-tenant (per-org) SSO provider configuration: the rows an iterion org admin self-serves to enable login via their own Keycloak (a discovery-based OIDC provider) or to gate the deployment-level GitHub login on specific GitHub teams.
It mirrors the per-tenant forge OAuth-app store (pkg/forge/oauth_app_store.go): one unified collection, rows discriminated by Kind, client secrets sealed at rest via secrets.Sealer with AAD "org_sso_provider:<id>". Get is keyed by id only — the HTTP layer asserts tenant ownership before mutating.
Security posture (see docs/cloud-admin.md): an org-admin-supplied issuer URL drives server-side discovery/token/userinfo fetches, so every outbound call is routed through the shared SSRF guard (pkg/secure/httpdial). SSO grant roles can never be `owner` (a static invariant enforced in Validate) and the dynamic ceiling (≤ the configuring admin's role) is enforced by the route handler.
Index ¶
- Constants
- Variables
- func EmailDomain(email string) string
- func FlattenGitHubTeamKeys(grants []GitHubTeamGrant) []string
- func NewDomainToken() (string, error)
- func NormalizeDomain(s string) string
- func OpenClientSecret(sealer secrets.Sealer, providerID string, sealed []byte) (string, error)
- func ParseOIDCSlug(slug string) (id string, ok bool)
- func SealClientSecret(sealer secrets.Sealer, providerID, clientSecret string) ([]byte, error)
- func VerifyDomainTXT(ctx context.Context, lookup TXTLookupFunc, d VerifiedDomain) (bool, error)
- type DomainStore
- type GitHubTeamGrant
- type Kind
- type MemoryDomainStore
- func (m *MemoryDomainStore) Create(_ context.Context, d VerifiedDomain) error
- func (m *MemoryDomainStore) Delete(_ context.Context, id string) error
- func (m *MemoryDomainStore) Get(_ context.Context, id string) (VerifiedDomain, error)
- func (m *MemoryDomainStore) IsVerifiedForTenant(_ context.Context, tenantID, domain string) (bool, error)
- func (m *MemoryDomainStore) ListByTenant(_ context.Context, tenantID string) ([]VerifiedDomain, error)
- func (m *MemoryDomainStore) TenantsForDomain(_ context.Context, domain string) ([]string, error)
- func (m *MemoryDomainStore) Update(_ context.Context, d VerifiedDomain) error
- type MemoryStore
- func (m *MemoryStore) Create(_ context.Context, p OrgSSOProvider) error
- func (m *MemoryStore) Delete(_ context.Context, id string) error
- func (m *MemoryStore) FindGitHubGrantingOrgs(_ context.Context, keys []string) ([]OrgSSOProvider, error)
- func (m *MemoryStore) Get(_ context.Context, id string) (OrgSSOProvider, error)
- func (m *MemoryStore) GitHubGatingActive(_ context.Context) (bool, error)
- func (m *MemoryStore) ListByTenant(_ context.Context, tenantID string) ([]OrgSSOProvider, error)
- func (m *MemoryStore) ListByTenantKind(_ context.Context, tenantID string, kind Kind) ([]OrgSSOProvider, error)
- func (m *MemoryStore) Update(_ context.Context, p OrgSSOProvider) error
- type MongoDomainStore
- func (s *MongoDomainStore) Create(ctx context.Context, d VerifiedDomain) error
- func (s *MongoDomainStore) Delete(ctx context.Context, id string) error
- func (s *MongoDomainStore) EnsureSchema(ctx context.Context) error
- func (s *MongoDomainStore) Get(ctx context.Context, id string) (VerifiedDomain, error)
- func (s *MongoDomainStore) IsVerifiedForTenant(ctx context.Context, tenantID, domain string) (bool, error)
- func (s *MongoDomainStore) ListByTenant(ctx context.Context, tenantID string) ([]VerifiedDomain, error)
- func (s *MongoDomainStore) TenantsForDomain(ctx context.Context, domain string) ([]string, error)
- func (s *MongoDomainStore) Update(ctx context.Context, d VerifiedDomain) error
- type MongoStore
- func (s *MongoStore) Create(ctx context.Context, p OrgSSOProvider) error
- func (s *MongoStore) Delete(ctx context.Context, id string) error
- func (s *MongoStore) EnsureSchema(ctx context.Context) error
- func (s *MongoStore) FindGitHubGrantingOrgs(ctx context.Context, keys []string) ([]OrgSSOProvider, error)
- func (s *MongoStore) Get(ctx context.Context, id string) (OrgSSOProvider, error)
- func (s *MongoStore) GitHubGatingActive(ctx context.Context) (bool, error)
- func (s *MongoStore) ListByTenant(ctx context.Context, tenantID string) ([]OrgSSOProvider, error)
- func (s *MongoStore) ListByTenantKind(ctx context.Context, tenantID string, kind Kind) ([]OrgSSOProvider, error)
- func (s *MongoStore) Update(ctx context.Context, p OrgSSOProvider) error
- type OrgSSOProvider
- type Store
- type TXTLookupFunc
- type VerifiedDomain
Constants ¶
const SSOProvidersCollectionName = "org_sso_providers"
SSOProvidersCollectionName is the Mongo collection backing the per-tenant SSO provider rows.
const VerifiedDomainsCollectionName = "org_verified_domains"
VerifiedDomainsCollectionName is the Mongo collection backing tenant domain claims.
Variables ¶
var ( ErrDomainNotFound = errors.New("orgsso: domain not found") ErrDomainExists = errors.New("orgsso: domain already claimed for this org") ErrDomainInvalid = errors.New("orgsso: invalid domain") )
var ( ErrNotFound = errors.New("orgsso: provider not found") ErrExists = errors.New("orgsso: a github provider already exists for this org") ErrInvalid = errors.New("orgsso: invalid provider definition") ErrOwnerNotGrant = errors.New("orgsso: owner cannot be granted via SSO") )
Sentinel errors. The HTTP layer maps these to status codes.
Functions ¶
func EmailDomain ¶
EmailDomain extracts the lowercased domain from an email address.
func FlattenGitHubTeamKeys ¶
func FlattenGitHubTeamKeys(grants []GitHubTeamGrant) []string
FlattenGitHubTeamKeys derives the materialised GitHubTeamKeys from a grant list (lowercased, deduped). Wildcard grants collapse to "<org>/*". Only VERIFIED grants are included — an unverified grant (the team hasn't proven it controls that GitHub org) is stored but inert at login.
func NewDomainToken ¶
NewDomainToken returns a random challenge token (~32 bytes base64url).
func NormalizeDomain ¶
NormalizeDomain lowercases + trims a domain, stripping a leading "@" or "*." and any scheme/path an admin might paste.
func OpenClientSecret ¶
OpenClientSecret returns an OIDC provider's client_secret from its sealed blob.
func ParseOIDCSlug ¶
ParseOIDCSlug extracts the provider ID from a per-org OIDC slug. ok is false for global connector slugs (which the caller resolves from the static registry instead).
func SealClientSecret ¶
SealClientSecret seals an OIDC provider's client_secret, binding the sealed blob to the provider id via AAD "org_sso_provider:<id>" (same convention as forge_oauth_app:<id> / generic_secret:<id>) so a sealed payload can't be silently transplanted onto another provider record or tenant.
func VerifyDomainTXT ¶
func VerifyDomainTXT(ctx context.Context, lookup TXTLookupFunc, d VerifiedDomain) (bool, error)
VerifyDomainTXT reports whether the domain's challenge TXT record is present.
Types ¶
type DomainStore ¶
type DomainStore interface {
Create(ctx context.Context, d VerifiedDomain) error
Get(ctx context.Context, id string) (VerifiedDomain, error)
Update(ctx context.Context, d VerifiedDomain) error
Delete(ctx context.Context, id string) error
ListByTenant(ctx context.Context, tenantID string) ([]VerifiedDomain, error)
// IsVerifiedForTenant reports whether domain is a VERIFIED claim of tenantID
// — the auto-link gate's lookup.
IsVerifiedForTenant(ctx context.Context, tenantID, domain string) (bool, error)
// TenantsForDomain returns the tenant ids that have VERIFIED the domain —
// the login screen's "discover an org's SSO from the user's email domain"
// lookup. Returns an empty slice (never an error) for an unclaimed domain so
// the providers endpoint stays a non-oracle.
TenantsForDomain(ctx context.Context, domain string) ([]string, error)
}
DomainStore persists per-tenant verified-domain claims. Get is keyed by id only — the HTTP layer asserts tenant ownership before mutating.
type GitHubTeamGrant ¶
type GitHubTeamGrant struct {
GitHubOrg string `bson:"github_org" json:"github_org"`
GitHubOrgID int64 `bson:"github_org_id,omitempty" json:"github_org_id,omitempty"`
TeamSlug string `bson:"team_slug,omitempty" json:"team_slug,omitempty"`
TeamID int64 `bson:"team_id,omitempty" json:"team_id,omitempty"`
Role identity.Role `bson:"role" json:"role"`
// Verified is set by the route handler once the org has proven control of
// GitHubOrg (Phase 2: a verified forge connection). An unverified grant is
// stored but inert at login time — surfaced as "pending verification".
Verified bool `bson:"verified,omitempty" json:"verified"`
}
GitHubTeamGrant maps a (GitHub org, team) to an iterion role. TeamSlug "" or "*" matches any team in (i.e. plain membership of) the GitHub org. Numeric IDs are captured for stable matching across org/team renames; the login path matches on IDs when present, falling back to lowercased login/slug.
type Kind ¶
type Kind string
Kind discriminates the two provider shapes stored in the unified collection.
const ( // KindOIDC is a per-org discovery-based OIDC provider (Keycloak, Auth0, // Azure AD, …). Carries issuer URL + client credentials. KindOIDC Kind = "oidc" // KindGitHub gates the deployment-level GitHub login on allow-listed // GitHub teams. Carries no credentials of its own (it reuses the global // GitHub OAuth app); only the grant list + auto-provision policy. KindGitHub Kind = "github" )
type MemoryDomainStore ¶
type MemoryDomainStore struct {
// contains filtered or unexported fields
}
func NewMemoryDomainStore ¶
func NewMemoryDomainStore() *MemoryDomainStore
func (*MemoryDomainStore) Create ¶
func (m *MemoryDomainStore) Create(_ context.Context, d VerifiedDomain) error
func (*MemoryDomainStore) Delete ¶
func (m *MemoryDomainStore) Delete(_ context.Context, id string) error
func (*MemoryDomainStore) Get ¶
func (m *MemoryDomainStore) Get(_ context.Context, id string) (VerifiedDomain, error)
func (*MemoryDomainStore) IsVerifiedForTenant ¶
func (*MemoryDomainStore) ListByTenant ¶
func (m *MemoryDomainStore) ListByTenant(_ context.Context, tenantID string) ([]VerifiedDomain, error)
func (*MemoryDomainStore) TenantsForDomain ¶
func (*MemoryDomainStore) Update ¶
func (m *MemoryDomainStore) Update(_ context.Context, d VerifiedDomain) error
type MemoryStore ¶
type MemoryStore struct {
// contains filtered or unexported fields
}
MemoryStore is an in-process Store for tests and local mode.
func NewMemoryStore ¶
func NewMemoryStore() *MemoryStore
NewMemoryStore returns an empty in-memory store.
func (*MemoryStore) Create ¶
func (m *MemoryStore) Create(_ context.Context, p OrgSSOProvider) error
func (*MemoryStore) FindGitHubGrantingOrgs ¶
func (m *MemoryStore) FindGitHubGrantingOrgs(_ context.Context, keys []string) ([]OrgSSOProvider, error)
func (*MemoryStore) Get ¶
func (m *MemoryStore) Get(_ context.Context, id string) (OrgSSOProvider, error)
func (*MemoryStore) GitHubGatingActive ¶
func (m *MemoryStore) GitHubGatingActive(_ context.Context) (bool, error)
func (*MemoryStore) ListByTenant ¶
func (m *MemoryStore) ListByTenant(_ context.Context, tenantID string) ([]OrgSSOProvider, error)
func (*MemoryStore) ListByTenantKind ¶
func (m *MemoryStore) ListByTenantKind(_ context.Context, tenantID string, kind Kind) ([]OrgSSOProvider, error)
func (*MemoryStore) Update ¶
func (m *MemoryStore) Update(_ context.Context, p OrgSSOProvider) error
type MongoDomainStore ¶
type MongoDomainStore struct {
// contains filtered or unexported fields
}
MongoDomainStore is the production DomainStore.
func NewMongoDomainStore ¶
func NewMongoDomainStore(db *mongo.Database) *MongoDomainStore
NewMongoDomainStore wires a MongoDomainStore on the given database.
func (*MongoDomainStore) Create ¶
func (s *MongoDomainStore) Create(ctx context.Context, d VerifiedDomain) error
func (*MongoDomainStore) Delete ¶
func (s *MongoDomainStore) Delete(ctx context.Context, id string) error
func (*MongoDomainStore) EnsureSchema ¶
func (s *MongoDomainStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates the unique (tenant_id, domain) index.
func (*MongoDomainStore) Get ¶
func (s *MongoDomainStore) Get(ctx context.Context, id string) (VerifiedDomain, error)
func (*MongoDomainStore) IsVerifiedForTenant ¶
func (*MongoDomainStore) ListByTenant ¶
func (s *MongoDomainStore) ListByTenant(ctx context.Context, tenantID string) ([]VerifiedDomain, error)
func (*MongoDomainStore) TenantsForDomain ¶
func (*MongoDomainStore) Update ¶
func (s *MongoDomainStore) Update(ctx context.Context, d VerifiedDomain) error
type MongoStore ¶
type MongoStore struct {
// contains filtered or unexported fields
}
MongoStore is the production Store.
func NewMongoStore ¶
func NewMongoStore(db *mongo.Database) *MongoStore
NewMongoStore wires a MongoStore on the given database.
func (*MongoStore) Create ¶
func (s *MongoStore) Create(ctx context.Context, p OrgSSOProvider) error
func (*MongoStore) EnsureSchema ¶
func (s *MongoStore) EnsureSchema(ctx context.Context) error
EnsureSchema creates the indexes:
- (tenant_id, kind): the general per-tenant listing index.
- partial-unique (tenant_id) where kind="github": at most one GitHub gating row per org (a single allow-list; the Grants array composes multiple GitHub orgs/teams inside it). A distinct key pattern from the listing index so the two don't collide.
- partial multikey (github_team_keys) where kind="github" & enabled: the reverse-lookup index that turns a GitHub login into one $in query.
func (*MongoStore) FindGitHubGrantingOrgs ¶
func (s *MongoStore) FindGitHubGrantingOrgs(ctx context.Context, keys []string) ([]OrgSSOProvider, error)
func (*MongoStore) Get ¶
func (s *MongoStore) Get(ctx context.Context, id string) (OrgSSOProvider, error)
func (*MongoStore) GitHubGatingActive ¶
func (s *MongoStore) GitHubGatingActive(ctx context.Context) (bool, error)
func (*MongoStore) ListByTenant ¶
func (s *MongoStore) ListByTenant(ctx context.Context, tenantID string) ([]OrgSSOProvider, error)
func (*MongoStore) ListByTenantKind ¶
func (s *MongoStore) ListByTenantKind(ctx context.Context, tenantID string, kind Kind) ([]OrgSSOProvider, error)
func (*MongoStore) Update ¶
func (s *MongoStore) Update(ctx context.Context, p OrgSSOProvider) error
type OrgSSOProvider ¶
type OrgSSOProvider struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
Kind Kind `bson:"kind" json:"kind"`
Enabled bool `bson:"enabled" json:"enabled"`
DisplayName string `bson:"display_name,omitempty" json:"display_name,omitempty"`
// IssuerURL is the OIDC issuer (no trailing slash); discovery hits
// <IssuerURL>/.well-known/openid-configuration. Stored in the clear.
IssuerURL string `bson:"issuer_url,omitempty" json:"issuer_url,omitempty"`
// ClientID is stored in the clear (the admin UI lists it). SealedSecret
// holds the client_secret sealed via secrets.Sealer with AAD
// "org_sso_provider:<ID>" — never serialised out of the server.
ClientID string `bson:"client_id,omitempty" json:"client_id,omitempty"`
SealedSecret []byte `bson:"sealed_secret,omitempty" json:"-"`
Scopes []string `bson:"scopes,omitempty" json:"scopes,omitempty"`
// DefaultRole is the membership role granted to a user who logs in via
// this OIDC provider for this org. Defaults to member; never owner.
DefaultRole identity.Role `bson:"default_role,omitempty" json:"default_role,omitempty"`
// AutoLinkOnEmail is a Phase-3 opt-in (default off): auto-link a fresh
// OIDC identity onto an existing iterion user matched by verified email.
// Stored now so the shape is stable; NOT acted upon until JWKS ID-token
// verification lands (the safe-auto-link prerequisite).
AutoLinkOnEmail bool `bson:"auto_link_on_email,omitempty" json:"auto_link_on_email,omitempty"`
// Grants maps (GitHub org, team) → iterion role. Evaluated at GitHub
// login: a user whose GitHub teams intersect Grants is granted membership
// in this org (Phase 2). Ordered: the first matching grant wins.
Grants []GitHubTeamGrant `bson:"grants,omitempty" json:"grants,omitempty"`
// GitHubTeamKeys is the materialised reverse-lookup view of Grants
// ("<org>/<team_slug>" + "<org>/*", lowercased), maintained on write so a
// GitHub login resolves matching orgs with one $in query instead of a
// cross-tenant scan. Internal — never serialised.
GitHubTeamKeys []string `bson:"github_team_keys,omitempty" json:"-"`
// AutoProvision: when true a matching user is auto-added to this org; when
// false, login is allowed only if a membership already exists (the admin
// must invite first).
AutoProvision bool `bson:"auto_provision,omitempty" json:"auto_provision"`
CreatedBy string `bson:"created_by" json:"created_by"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"`
}
OrgSSOProvider is one per-tenant SSO configuration row.
func (*OrgSSOProvider) Normalize ¶
func (p *OrgSSOProvider) Normalize()
Normalize canonicalises a row in place before persistence: trims the issuer URL, defaults scopes/role, and (re)materialises GitHubTeamKeys.
func (OrgSSOProvider) OIDCSlug ¶
func (p OrgSSOProvider) OIDCSlug() string
OIDCSlug returns the URL slug used for this provider's /api/auth/oidc/<slug>/… routes. Only meaningful for KindOIDC rows.
func (OrgSSOProvider) RoleForGroups ¶
func (p OrgSSOProvider) RoleForGroups(groups []string) (identity.Role, bool)
RoleForGroups returns the role this GitHub row grants a user holding the given (lowercased) group keys — the first matching grant in declaration order, clamped to at most member. ok is false when no grant matches.
func (*OrgSSOProvider) Validate ¶
func (p *OrgSSOProvider) Validate() error
Validate enforces the kind-specific contract + the static SSO security invariants (https issuer, no owner grant, valid roles). Call after Normalize.
type Store ¶
type Store interface {
Create(ctx context.Context, p OrgSSOProvider) error
Get(ctx context.Context, id string) (OrgSSOProvider, error)
Update(ctx context.Context, p OrgSSOProvider) error
Delete(ctx context.Context, id string) error
// ListByTenant returns every provider for a tenant, oldest first.
ListByTenant(ctx context.Context, tenantID string) ([]OrgSSOProvider, error)
// ListByTenantKind narrows ListByTenant to one Kind (e.g. enabled OIDC
// rows for the org-scoped login picker).
ListByTenantKind(ctx context.Context, tenantID string, kind Kind) ([]OrgSSOProvider, error)
// FindGitHubGrantingOrgs is the load-bearing reverse lookup for GitHub
// team-gating: every ENABLED KindGitHub row whose GitHubTeamKeys intersect
// keys. Backed by a multikey $in index — never a cross-tenant scan.
FindGitHubGrantingOrgs(ctx context.Context, keys []string) ([]OrgSSOProvider, error)
// GitHubGatingActive reports whether any enabled KindGitHub row exists at
// all. When true, a GitHub login that matches no allow-listed team and has
// no prior access is refused (ErrSSORestricted); when false, GitHub login
// behaves as before (no team-gating in this deployment).
GitHubGatingActive(ctx context.Context) (bool, error)
}
Store persists per-tenant SSO provider rows. Get is keyed by id only — the HTTP layer asserts tenant ownership (row.TenantID == teamID) before mutating, matching the forge OAuthAppStore convention.
type TXTLookupFunc ¶
TXTLookupFunc resolves the TXT records for a name. Injectable for tests; the production default wraps net.Resolver.LookupTXT.
func DefaultTXTLookup ¶
func DefaultTXTLookup() TXTLookupFunc
DefaultTXTLookup returns the production net-backed TXT lookup.
type VerifiedDomain ¶
type VerifiedDomain struct {
ID string `bson:"_id" json:"id"`
TenantID string `bson:"tenant_id" json:"tenant_id"`
Domain string `bson:"domain" json:"domain"` // lowercased, no leading "@"
Token string `bson:"token" json:"token"` // the TXT challenge value
VerifiedAt *time.Time `bson:"verified_at,omitempty" json:"verified_at,omitempty"`
CreatedBy string `bson:"created_by,omitempty" json:"created_by,omitempty"`
CreatedAt time.Time `bson:"created_at" json:"created_at"`
}
VerifiedDomain records a tenant's claim over an email domain, proven by a DNS TXT challenge. It gates per-org SSO auto-link: an org's Keycloak may only auto-link a fresh OIDC identity onto an existing iterion user by email when the email's domain is verified for that org — so a malicious org IdP can't claim email_verified for an address outside the org's domain authority and take over an unrelated account (the H-16 account-takeover gate; JWKS alone is insufficient because the org controls its own IdP signing keys).
func (VerifiedDomain) ChallengeHost ¶
func (d VerifiedDomain) ChallengeHost() string
ChallengeHost is the DNS name the admin must create a TXT record at.
func (VerifiedDomain) ChallengeValue ¶
func (d VerifiedDomain) ChallengeValue() string
ChallengeValue is the exact TXT record value to publish.
func (VerifiedDomain) Verified ¶
func (d VerifiedDomain) Verified() bool
Verified reports whether the domain claim has been DNS-verified.