package identity import ( "errors" "net" "net/url" "strings" "time" "github.com/google/uuid" ) var ( ErrRevisionNotFound = errors.New("identity configuration revision not found") ErrRevisionConflict = errors.New("identity configuration revision conflicts with current state") ErrBreakGlassRequired = errors.New("a local break-glass manager credential is required") ErrLocalTenantInvalid = errors.New("local tenant mapping is invalid") ) type RevisionPolicy struct { LocalTenantKey string `json:"localTenantKey"` RolePrefix string `json:"rolePrefix"` JITEnabled bool `json:"jitEnabled"` LegacyJWTEnabled bool `json:"legacyJwtEnabled"` SessionIdleSeconds int `json:"sessionIdleSeconds"` SessionAbsoluteSeconds int `json:"sessionAbsoluteSeconds"` SessionRefreshSeconds int `json:"sessionRefreshSeconds"` } func (policy RevisionPolicy) Validate() error { if strings.TrimSpace(policy.LocalTenantKey) == "" || strings.TrimSpace(policy.RolePrefix) == "" { return errors.New("local tenant mapping and role prefix are required") } if policy.SessionIdleSeconds <= 0 || policy.SessionAbsoluteSeconds <= policy.SessionIdleSeconds || policy.SessionRefreshSeconds <= 0 || policy.SessionRefreshSeconds >= policy.SessionIdleSeconds { return errors.New("identity session policy is invalid") } return nil } type RevisionState string const ( RevisionDraft RevisionState = "draft" RevisionValidated RevisionState = "validated" RevisionActive RevisionState = "active" RevisionSuperseded RevisionState = "superseded" RevisionFailed RevisionState = "failed" ) type Revision struct { ID string `json:"id"` State RevisionState `json:"state"` SchemaVersion int `json:"schemaVersion"` AuthCenterURL string `json:"authCenterUrl"` Issuer string `json:"issuer,omitempty"` TenantID string `json:"tenantId,omitempty"` ApplicationID string `json:"applicationId,omitempty"` Audience string `json:"audience,omitempty"` BrowserClientID string `json:"browserClientId,omitempty"` MachineClientID string `json:"machineClientId,omitempty"` Scopes []string `json:"scopes"` Capabilities []string `json:"capabilities"` RolePrefix string `json:"rolePrefix"` LocalTenantKey string `json:"localTenantKey"` PublicBaseURL string `json:"publicBaseUrl"` WebBaseURL string `json:"webBaseUrl"` JITEnabled bool `json:"jitEnabled"` LegacyJWTEnabled bool `json:"legacyJwtEnabled"` TokenIntrospection bool `json:"tokenIntrospection"` SessionRevocation bool `json:"sessionRevocation"` SecurityEventIssuer string `json:"securityEventIssuer,omitempty"` SecurityEventConfigURL string `json:"securityEventConfigurationUrl,omitempty"` SecurityEventAudience string `json:"securityEventAudience,omitempty"` MachineCredentialRef string `json:"-"` SessionEncryptionKeyRef string `json:"-"` SessionIdleSeconds int `json:"sessionIdleSeconds"` SessionAbsoluteSeconds int `json:"sessionAbsoluteSeconds"` SessionRefreshSeconds int `json:"sessionRefreshSeconds"` Version int64 `json:"version"` LastErrorCategory string `json:"lastErrorCategory,omitempty"` LastTraceID string `json:"lastTraceId,omitempty"` LastAuditID string `json:"lastAuditId,omitempty"` ValidatedAt *time.Time `json:"validatedAt,omitempty"` ActivatedAt *time.Time `json:"activatedAt,omitempty"` SupersededAt *time.Time `json:"supersededAt,omitempty"` CreatedAt time.Time `json:"createdAt"` UpdatedAt time.Time `json:"updatedAt"` } type PairingInput struct { AuthCenterURL string `json:"authCenterUrl"` OnboardingCode string `json:"onboardingCode"` PublicBaseURL string `json:"publicBaseUrl"` WebBaseURL string `json:"webBaseUrl"` LocalTenantKey string `json:"localTenantKey"` LegacyJWTEnabled bool `json:"legacyJwtEnabled"` } type ConsumerMetadata struct { PublicBaseURL string `json:"public_base_url"` WebBaseURL string `json:"web_base_url"` RedirectURIs []string `json:"redirect_uris"` LogoutURIs []string `json:"logout_uris"` ReceiverEndpoint string `json:"receiver_endpoint,omitempty"` } type ManifestApplication struct { Manifest ManifestV1 MachineCredentialRef string SessionEncryptionKeyRef string TraceID string AuditID string } func NewDraft(input PairingInput) (Revision, error) { if _, err := input.ConsumerMetadata(false); err != nil { return Revision{}, err } authCenter, _ := exactBaseURL(input.AuthCenterURL) publicBase, _ := exactBaseURL(input.PublicBaseURL) webBase, _ := exactBaseURL(input.WebBaseURL) return Revision{ ID: uuid.NewString(), State: RevisionDraft, SchemaVersion: 1, AuthCenterURL: authCenter, RolePrefix: "gateway.", LocalTenantKey: strings.TrimSpace(input.LocalTenantKey), PublicBaseURL: publicBase, WebBaseURL: webBase, JITEnabled: true, LegacyJWTEnabled: input.LegacyJWTEnabled, Scopes: []string{}, Capabilities: []string{}, SessionIdleSeconds: 1800, SessionAbsoluteSeconds: 28800, SessionRefreshSeconds: 60, Version: 1, }, nil } func ApplyManifest(revision Revision, input ManifestApplication) (Revision, error) { if revision.State != RevisionDraft { return Revision{}, ErrRevisionConflict } if err := input.Manifest.Validate(); err != nil { return Revision{}, err } capabilities := make(map[string]bool, len(input.Manifest.Capabilities)) for _, capability := range input.Manifest.Capabilities { capabilities[capability] = true } if capabilities["machine_to_machine"] && strings.TrimSpace(input.MachineCredentialRef) == "" { return Revision{}, errors.New("machine credential reference is required") } if capabilities["oidc_login"] && strings.TrimSpace(input.SessionEncryptionKeyRef) == "" { return Revision{}, errors.New("session encryption key reference is required") } revision.Issuer = strings.TrimRight(input.Manifest.Issuer, "/") revision.TenantID = input.Manifest.TenantID revision.ApplicationID = input.Manifest.ApplicationID revision.Audience = input.Manifest.Audience revision.Scopes = append([]string(nil), input.Manifest.Scopes...) revision.Capabilities = append([]string(nil), input.Manifest.Capabilities...) if input.Manifest.Clients.BrowserLogin != nil { revision.BrowserClientID = input.Manifest.Clients.BrowserLogin.ClientID } if input.Manifest.Clients.MachineToMachine != nil { revision.MachineClientID = input.Manifest.Clients.MachineToMachine.ClientID } revision.TokenIntrospection = capabilities["token_introspection"] revision.SessionRevocation = capabilities["session_revocation"] if input.Manifest.SecurityEvents != nil { revision.SecurityEventIssuer = input.Manifest.SecurityEvents.TransmitterIssuer revision.SecurityEventConfigURL = input.Manifest.SecurityEvents.ConfigurationEndpoint revision.SecurityEventAudience = input.Manifest.SecurityEvents.Audience } revision.MachineCredentialRef = strings.TrimSpace(input.MachineCredentialRef) revision.SessionEncryptionKeyRef = strings.TrimSpace(input.SessionEncryptionKeyRef) revision.LastTraceID = strings.TrimSpace(input.TraceID) revision.LastAuditID = strings.TrimSpace(input.AuditID) return revision, nil } func CanTransition(from, to RevisionState) bool { switch from { case RevisionDraft: return to == RevisionValidated || to == RevisionFailed case RevisionValidated: return to == RevisionActive || to == RevisionFailed case RevisionActive: return to == RevisionSuperseded case RevisionSuperseded: return to == RevisionValidated default: return false } } func (input PairingInput) ConsumerMetadata(sessionRevocation bool) (ConsumerMetadata, error) { authCenter, err := exactBaseURL(input.AuthCenterURL) if err != nil { return ConsumerMetadata{}, errors.New("auth center URL is invalid") } _ = authCenter publicBase, err := exactBaseURL(input.PublicBaseURL) if err != nil { return ConsumerMetadata{}, errors.New("public base URL is invalid") } webBase, err := exactBaseURL(input.WebBaseURL) if err != nil { return ConsumerMetadata{}, errors.New("web base URL is invalid") } if strings.TrimSpace(input.LocalTenantKey) == "" { return ConsumerMetadata{}, errors.New("local tenant mapping is required") } metadata := ConsumerMetadata{ PublicBaseURL: publicBase, WebBaseURL: webBase, RedirectURIs: []string{publicBase + "/api/v1/auth/oidc/callback"}, LogoutURIs: []string{webBase + "/"}, } if sessionRevocation { metadata.ReceiverEndpoint = publicBase + "/api/v1/security-events/ssf" } return metadata, nil } func exactBaseURL(raw string) (string, error) { parsed, err := url.Parse(strings.TrimSpace(raw)) if err != nil || parsed.Opaque != "" || parsed.User != nil || parsed.Host == "" || parsed.RawQuery != "" || parsed.Fragment != "" { return "", errors.New("invalid public URL") } hostname := strings.ToLower(parsed.Hostname()) if parsed.Path != "" && parsed.Path != "/" { return "", errors.New("base URL must not contain a path") } scheme := strings.ToLower(parsed.Scheme) if scheme != "https" && !(scheme == "http" && isLoopbackHost(hostname)) { return "", errors.New("public URL must use HTTPS") } port := parsed.Port() if scheme == "https" && port == "443" || scheme == "http" && port == "80" { port = "" } host := hostname if strings.Contains(hostname, ":") { host = "[" + hostname + "]" } if port != "" { host = net.JoinHostPort(hostname, port) } return scheme + "://" + host, nil } func isLoopbackHost(host string) bool { if host == "localhost" { return true } ip := net.ParseIP(host) return ip != nil && ip.IsLoopback() }