Skip to content

Commit f8a1b85

Browse files
committed
feat: add ServiceAccount and TrustedIssuer schemas, storage interface, and M2M constants
Introduces the two new storage entities for the workload identity program: - ServiceAccount: machine/workload OAuth2 client (client_id = UUID, client_secret = bcrypt hash). Intentionally omits all human-user fields. - TrustedIssuer: external JWT issuer registry for K8s SA tokens, SPIFFE JWT-SVIDs, and generic OIDC workload tokens. Carries all fields needed for Phases 1-6: KeySourceType, ExpectedAud, EnableTokenReview (Phase 4), SpiffeRefreshHintSeconds (Phase 5), AuthMethod + proxy fields (Phase 6). Storage changes: - Added ServiceAccount and TrustedIssuer to CollectionList and Collections - Extended storage.Provider interface with 5 ServiceAccount + 6 TrustedIssuer methods - SQL provider: full GORM CRUD implementation + AutoMigrate registration - NoSQL providers (mongodb, arangodb, cassandradb, dynamodb, couchbase): stubs returning not-implemented errors, to be replaced in PR 3 Constants: - grant_types.go: RFC 6749/8693 grant types, RFC 7521/7523 client assertion types, RFC 8693 token type URNs, and TrustedIssuer key-source/issuer-type/ auth-method identifiers - audit_event.go: service_account and trusted_issuer audit events + resource types Spec: docs/specs/WORKLOAD_IDENTITY_PROGRAM.md
1 parent 9a43e98 commit f8a1b85

20 files changed

Lines changed: 1615 additions & 1 deletion

docs/specs/WORKLOAD_IDENTITY_PROGRAM.md

Lines changed: 702 additions & 0 deletions
Large diffs are not rendered by default.

internal/constants/audit_event.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ const (
2626
AuditResourceTypeFgaModel = "fga_model"
2727
// AuditResourceTypeFgaTuple represents a fine-grained authorization tuple.
2828
AuditResourceTypeFgaTuple = "fga_tuple"
29+
// AuditResourceTypeServiceAccount represents a machine/workload service account.
30+
AuditResourceTypeServiceAccount = "service_account"
31+
// AuditResourceTypeTrustedIssuer represents a trusted external JWT issuer.
32+
AuditResourceTypeTrustedIssuer = "trusted_issuer"
2933
)
3034

3135
// Audit event type constants used for structured audit logging.
@@ -126,4 +130,29 @@ const (
126130
AuditSessionCreatedEvent = "session.created"
127131
// AuditSessionTerminatedEvent is logged when a session is terminated.
128132
AuditSessionTerminatedEvent = "session.terminated"
133+
134+
// AuditServiceAccountCreatedEvent is logged when an admin creates a service account.
135+
AuditServiceAccountCreatedEvent = "admin.service_account_created"
136+
// AuditServiceAccountUpdatedEvent is logged when an admin updates a service account.
137+
AuditServiceAccountUpdatedEvent = "admin.service_account_updated"
138+
// AuditServiceAccountDeletedEvent is logged when an admin deletes a service account.
139+
AuditServiceAccountDeletedEvent = "admin.service_account_deleted"
140+
// AuditServiceAccountSecretRotatedEvent is logged when a service account secret is rotated.
141+
AuditServiceAccountSecretRotatedEvent = "admin.service_account_secret_rotated"
142+
143+
// AuditTrustedIssuerCreatedEvent is logged when an admin adds a trusted issuer.
144+
AuditTrustedIssuerCreatedEvent = "admin.trusted_issuer_created"
145+
// AuditTrustedIssuerUpdatedEvent is logged when an admin updates a trusted issuer.
146+
AuditTrustedIssuerUpdatedEvent = "admin.trusted_issuer_updated"
147+
// AuditTrustedIssuerDeletedEvent is logged when an admin deletes a trusted issuer.
148+
AuditTrustedIssuerDeletedEvent = "admin.trusted_issuer_deleted"
149+
150+
// AuditTokenClientCredentialsEvent is logged when a service account obtains a token
151+
// via the client_credentials grant (RFC 6749 §4.4).
152+
AuditTokenClientCredentialsEvent = "token.client_credentials"
153+
// AuditTokenExchangeEvent is logged when a token exchange occurs (RFC 8693).
154+
AuditTokenExchangeEvent = "token.exchange"
155+
// AuditWorkloadAuthEvent is logged when a workload authenticates via client_assertion
156+
// (K8s SA token, SPIFFE JWT-SVID, or generic OIDC workload token).
157+
AuditWorkloadAuthEvent = "token.workload_auth"
129158
)

internal/constants/grant_types.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package constants
2+
3+
// OAuth2 grant type URNs for the token endpoint (RFC 6749 + RFC 8693).
4+
const (
5+
// GrantTypeAuthorizationCode is the standard browser-redirect grant (RFC 6749 §4.1).
6+
GrantTypeAuthorizationCode = "authorization_code"
7+
8+
// GrantTypeRefreshToken rotates an existing refresh token (RFC 6749 §6).
9+
GrantTypeRefreshToken = "refresh_token"
10+
11+
// GrantTypeClientCredentials issues tokens for machine/service identities
12+
// without a human resource owner (RFC 6749 §4.4).
13+
// Authenticate using client_id (= ServiceAccount.ID) and client_secret,
14+
// or via client_assertion for workload identity federation (Phases 3–5).
15+
GrantTypeClientCredentials = "client_credentials"
16+
17+
// GrantTypeTokenExchange enables on-behalf-of delegation and workload-to-user
18+
// token exchange (RFC 8693).
19+
GrantTypeTokenExchange = "urn:ietf:params:oauth:grant-type:token-exchange"
20+
)
21+
22+
// OAuth2 client assertion type URNs (RFC 7521 / RFC 7523).
23+
const (
24+
// ClientAssertionTypeJWTBearer identifies a JWT as the client credential
25+
// in the client_assertion parameter (RFC 7523).
26+
// Used for Kubernetes SA tokens and generic OIDC workload tokens (Phases 3–4).
27+
ClientAssertionTypeJWTBearer = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
28+
29+
// ClientAssertionTypeJWTSPIFFE identifies a SPIFFE JWT-SVID as the client
30+
// credential (draft-schwenkschuster-oauth-spiffe-client-auth-00).
31+
// PREVIEW: the underlying draft expired 2026-01-02 and is not WG-adopted.
32+
// This URN is not IANA-registered and may change. Ship as experimental only.
33+
ClientAssertionTypeJWTSPIFFE = "urn:ietf:params:oauth:client-assertion-type:jwt-spiffe"
34+
)
35+
36+
// RFC 8693 token type URNs used in subject_token_type and issued_token_type.
37+
const (
38+
// TokenTypeURNAccessToken identifies an OAuth2 access token.
39+
TokenTypeURNAccessToken = "urn:ietf:params:oauth:token-type:access_token"
40+
41+
// TokenTypeURNRefreshToken identifies an OAuth2 refresh token.
42+
TokenTypeURNRefreshToken = "urn:ietf:params:oauth:token-type:refresh_token"
43+
44+
// TokenTypeURNIDToken identifies an OpenID Connect ID token.
45+
TokenTypeURNIDToken = "urn:ietf:params:oauth:token-type:id_token"
46+
47+
// TokenTypeURNJWT identifies a generic JWT.
48+
TokenTypeURNJWT = "urn:ietf:params:oauth:token-type:jwt"
49+
)
50+
51+
// TrustedIssuer key source types controlling how JWKS are fetched.
52+
const (
53+
// KeySourceOIDCDiscovery fetches the JWKS URI from the issuer's OpenID
54+
// Connect discovery document (/.well-known/openid-configuration).
55+
// Not suitable for private K8s clusters that do not expose discovery publicly.
56+
KeySourceOIDCDiscovery = "oidc_discovery"
57+
58+
// KeySourceStaticJWKSURL fetches JWKS directly from a configured URL.
59+
// Preferred for private clusters — avoids exposing K8s discovery endpoints.
60+
KeySourceStaticJWKSURL = "static_jwks_url"
61+
62+
// KeySourceSPIFFEBundleEndpoint fetches keys from a SPIFFE bundle endpoint.
63+
// Requires SpiffeRefreshHintSeconds to be honoured at runtime (Phase 5).
64+
KeySourceSPIFFEBundleEndpoint = "spiffe_bundle_endpoint"
65+
)
66+
67+
// TrustedIssuer issuer type identifiers.
68+
const (
69+
// IssuerTypeKubernetesSA identifies a Kubernetes projected ServiceAccount token.
70+
IssuerTypeKubernetesSA = "kubernetes_sa"
71+
72+
// IssuerTypeSPIFFEJWT identifies a SPIFFE JWT-SVID (Phase 5, preview).
73+
IssuerTypeSPIFFEJWT = "spiffe_jwt"
74+
75+
// IssuerTypeOIDC identifies a generic OIDC token from an external IdP.
76+
IssuerTypeOIDC = "oidc"
77+
78+
// IssuerTypeCloudOIDC identifies a cloud-provider workload identity token
79+
// (AWS IRSA, GCP Workload Identity, Azure Managed Identity).
80+
IssuerTypeCloudOIDC = "cloud_oidc"
81+
)
82+
83+
// TrustedIssuer authentication method identifiers.
84+
const (
85+
// AuthMethodJWTAssertion uses a JWT as the client_assertion (Phases 3–5, default).
86+
AuthMethodJWTAssertion = "jwt_assertion"
87+
88+
// AuthMethodX509MTLS uses an X.509-SVID via mTLS (Phase 6).
89+
AuthMethodX509MTLS = "x509_mtls"
90+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package arangodb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/authorizerdev/authorizer/internal/graph/model"
8+
"github.com/authorizerdev/authorizer/internal/storage/schemas"
9+
)
10+
11+
// AddServiceAccount creates a new service account record.
12+
// TODO(phase1-pr3): implement ArangoDB provider.
13+
func (p *provider) AddServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
14+
return nil, fmt.Errorf("arangodb: AddServiceAccount not implemented")
15+
}
16+
17+
// UpdateServiceAccount updates a service account record.
18+
// TODO(phase1-pr3): implement ArangoDB provider.
19+
func (p *provider) UpdateServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
20+
return nil, fmt.Errorf("arangodb: UpdateServiceAccount not implemented")
21+
}
22+
23+
// DeleteServiceAccount removes a service account record.
24+
// TODO(phase1-pr3): implement ArangoDB provider.
25+
func (p *provider) DeleteServiceAccount(_ context.Context, _ *schemas.ServiceAccount) error {
26+
return fmt.Errorf("arangodb: DeleteServiceAccount not implemented")
27+
}
28+
29+
// GetServiceAccountByID fetches a service account by primary key.
30+
// TODO(phase1-pr3): implement ArangoDB provider.
31+
func (p *provider) GetServiceAccountByID(_ context.Context, _ string) (*schemas.ServiceAccount, error) {
32+
return nil, fmt.Errorf("arangodb: GetServiceAccountByID not implemented")
33+
}
34+
35+
// ListServiceAccounts returns a paginated list of service accounts.
36+
// TODO(phase1-pr3): implement ArangoDB provider.
37+
func (p *provider) ListServiceAccounts(_ context.Context, _ *model.Pagination) ([]*schemas.ServiceAccount, *model.Pagination, error) {
38+
return nil, nil, fmt.Errorf("arangodb: ListServiceAccounts not implemented")
39+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package arangodb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/authorizerdev/authorizer/internal/graph/model"
8+
"github.com/authorizerdev/authorizer/internal/storage/schemas"
9+
)
10+
11+
// AddTrustedIssuer creates a new trusted issuer record.
12+
// TODO(phase1-pr3): implement ArangoDB provider.
13+
func (p *provider) AddTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
14+
return nil, fmt.Errorf("arangodb: AddTrustedIssuer not implemented")
15+
}
16+
17+
// UpdateTrustedIssuer updates a trusted issuer record.
18+
// TODO(phase1-pr3): implement ArangoDB provider.
19+
func (p *provider) UpdateTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
20+
return nil, fmt.Errorf("arangodb: UpdateTrustedIssuer not implemented")
21+
}
22+
23+
// DeleteTrustedIssuer removes a trusted issuer record.
24+
// TODO(phase1-pr3): implement ArangoDB provider.
25+
func (p *provider) DeleteTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) error {
26+
return fmt.Errorf("arangodb: DeleteTrustedIssuer not implemented")
27+
}
28+
29+
// GetTrustedIssuerByID fetches a trusted issuer by primary key.
30+
// TODO(phase1-pr3): implement ArangoDB provider.
31+
func (p *provider) GetTrustedIssuerByID(_ context.Context, _ string) (*schemas.TrustedIssuer, error) {
32+
return nil, fmt.Errorf("arangodb: GetTrustedIssuerByID not implemented")
33+
}
34+
35+
// GetTrustedIssuerByIssuerURL fetches a trusted issuer by its unique issuer URL.
36+
// TODO(phase1-pr3): implement ArangoDB provider.
37+
func (p *provider) GetTrustedIssuerByIssuerURL(_ context.Context, _ string) (*schemas.TrustedIssuer, error) {
38+
return nil, fmt.Errorf("arangodb: GetTrustedIssuerByIssuerURL not implemented")
39+
}
40+
41+
// ListTrustedIssuers returns paginated trusted issuers, optionally filtered by serviceAccountID.
42+
// TODO(phase1-pr3): implement ArangoDB provider.
43+
func (p *provider) ListTrustedIssuers(_ context.Context, _ string, _ *model.Pagination) ([]*schemas.TrustedIssuer, *model.Pagination, error) {
44+
return nil, nil, fmt.Errorf("arangodb: ListTrustedIssuers not implemented")
45+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package cassandradb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/authorizerdev/authorizer/internal/graph/model"
8+
"github.com/authorizerdev/authorizer/internal/storage/schemas"
9+
)
10+
11+
// AddServiceAccount creates a new service account record.
12+
// TODO(phase1-pr3): implement CassandraDB provider.
13+
// DDL required before implementation:
14+
//
15+
// CREATE TABLE IF NOT EXISTS authorizer_service_accounts (
16+
// id text PRIMARY KEY,
17+
// name text, description text, client_secret text,
18+
// allowed_scopes text, is_active boolean,
19+
// created_at bigint, updated_at bigint
20+
// );
21+
func (p *provider) AddServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
22+
return nil, fmt.Errorf("cassandradb: AddServiceAccount not implemented")
23+
}
24+
25+
// UpdateServiceAccount updates a service account record.
26+
// TODO(phase1-pr3): implement CassandraDB provider.
27+
func (p *provider) UpdateServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
28+
return nil, fmt.Errorf("cassandradb: UpdateServiceAccount not implemented")
29+
}
30+
31+
// DeleteServiceAccount removes a service account record.
32+
// TODO(phase1-pr3): implement CassandraDB provider.
33+
func (p *provider) DeleteServiceAccount(_ context.Context, _ *schemas.ServiceAccount) error {
34+
return fmt.Errorf("cassandradb: DeleteServiceAccount not implemented")
35+
}
36+
37+
// GetServiceAccountByID fetches a service account by primary key.
38+
// TODO(phase1-pr3): implement CassandraDB provider.
39+
func (p *provider) GetServiceAccountByID(_ context.Context, _ string) (*schemas.ServiceAccount, error) {
40+
return nil, fmt.Errorf("cassandradb: GetServiceAccountByID not implemented")
41+
}
42+
43+
// ListServiceAccounts returns a paginated list of service accounts.
44+
// TODO(phase1-pr3): implement CassandraDB provider.
45+
func (p *provider) ListServiceAccounts(_ context.Context, _ *model.Pagination) ([]*schemas.ServiceAccount, *model.Pagination, error) {
46+
return nil, nil, fmt.Errorf("cassandradb: ListServiceAccounts not implemented")
47+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package cassandradb
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/authorizerdev/authorizer/internal/graph/model"
8+
"github.com/authorizerdev/authorizer/internal/storage/schemas"
9+
)
10+
11+
// AddTrustedIssuer creates a new trusted issuer record.
12+
// TODO(phase1-pr3): implement CassandraDB provider.
13+
// DDL required before implementation:
14+
//
15+
// CREATE TABLE IF NOT EXISTS authorizer_trusted_issuers (
16+
// id text PRIMARY KEY,
17+
// service_account_id text, name text, issuer_url text,
18+
// key_source_type text, jwks_url text, expected_aud text,
19+
// subject_claim text, issuer_type text, auth_method text,
20+
// is_active boolean, enable_token_review boolean,
21+
// kubernetes_api_server_url text,
22+
// spiffe_refresh_hint_seconds bigint,
23+
// trusted_proxy_header text, trusted_proxy_cidrs text,
24+
// created_at bigint, updated_at bigint
25+
// );
26+
func (p *provider) AddTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
27+
return nil, fmt.Errorf("cassandradb: AddTrustedIssuer not implemented")
28+
}
29+
30+
// UpdateTrustedIssuer updates a trusted issuer record.
31+
// TODO(phase1-pr3): implement CassandraDB provider.
32+
func (p *provider) UpdateTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) (*schemas.TrustedIssuer, error) {
33+
return nil, fmt.Errorf("cassandradb: UpdateTrustedIssuer not implemented")
34+
}
35+
36+
// DeleteTrustedIssuer removes a trusted issuer record.
37+
// TODO(phase1-pr3): implement CassandraDB provider.
38+
func (p *provider) DeleteTrustedIssuer(_ context.Context, _ *schemas.TrustedIssuer) error {
39+
return fmt.Errorf("cassandradb: DeleteTrustedIssuer not implemented")
40+
}
41+
42+
// GetTrustedIssuerByID fetches a trusted issuer by primary key.
43+
// TODO(phase1-pr3): implement CassandraDB provider.
44+
func (p *provider) GetTrustedIssuerByID(_ context.Context, _ string) (*schemas.TrustedIssuer, error) {
45+
return nil, fmt.Errorf("cassandradb: GetTrustedIssuerByID not implemented")
46+
}
47+
48+
// GetTrustedIssuerByIssuerURL fetches a trusted issuer by its unique issuer URL.
49+
// TODO(phase1-pr3): implement CassandraDB provider.
50+
func (p *provider) GetTrustedIssuerByIssuerURL(_ context.Context, _ string) (*schemas.TrustedIssuer, error) {
51+
return nil, fmt.Errorf("cassandradb: GetTrustedIssuerByIssuerURL not implemented")
52+
}
53+
54+
// ListTrustedIssuers returns paginated trusted issuers, optionally filtered by serviceAccountID.
55+
// TODO(phase1-pr3): implement CassandraDB provider.
56+
func (p *provider) ListTrustedIssuers(_ context.Context, _ string, _ *model.Pagination) ([]*schemas.TrustedIssuer, *model.Pagination, error) {
57+
return nil, nil, fmt.Errorf("cassandradb: ListTrustedIssuers not implemented")
58+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package couchbase
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/authorizerdev/authorizer/internal/graph/model"
8+
"github.com/authorizerdev/authorizer/internal/storage/schemas"
9+
)
10+
11+
// AddServiceAccount creates a new service account record.
12+
// TODO(phase1-pr3): implement Couchbase provider.
13+
func (p *provider) AddServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
14+
return nil, fmt.Errorf("couchbase: AddServiceAccount not implemented")
15+
}
16+
17+
// UpdateServiceAccount updates a service account record.
18+
// TODO(phase1-pr3): implement Couchbase provider.
19+
func (p *provider) UpdateServiceAccount(_ context.Context, _ *schemas.ServiceAccount) (*schemas.ServiceAccount, error) {
20+
return nil, fmt.Errorf("couchbase: UpdateServiceAccount not implemented")
21+
}
22+
23+
// DeleteServiceAccount removes a service account record.
24+
// TODO(phase1-pr3): implement Couchbase provider.
25+
func (p *provider) DeleteServiceAccount(_ context.Context, _ *schemas.ServiceAccount) error {
26+
return fmt.Errorf("couchbase: DeleteServiceAccount not implemented")
27+
}
28+
29+
// GetServiceAccountByID fetches a service account by primary key.
30+
// TODO(phase1-pr3): implement Couchbase provider.
31+
func (p *provider) GetServiceAccountByID(_ context.Context, _ string) (*schemas.ServiceAccount, error) {
32+
return nil, fmt.Errorf("couchbase: GetServiceAccountByID not implemented")
33+
}
34+
35+
// ListServiceAccounts returns a paginated list of service accounts.
36+
// TODO(phase1-pr3): implement Couchbase provider.
37+
func (p *provider) ListServiceAccounts(_ context.Context, _ *model.Pagination) ([]*schemas.ServiceAccount, *model.Pagination, error) {
38+
return nil, nil, fmt.Errorf("couchbase: ListServiceAccounts not implemented")
39+
}

0 commit comments

Comments
 (0)