diff --git a/api/openapi.yaml b/api/openapi.yaml index 31ec2f51..d042ff9d 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -3097,6 +3097,37 @@ paths: description: Revoked (idempotent) '403': {$ref: '#/components/responses/Forbidden'} + # Spec: api-auth-policy — workspace authentication policy. + /api/v1/auth-policy: + get: + operationId: getAuthPolicy + summary: View the workspace authentication policy + x-required-permission: system:auth_policy_read + responses: + '200': + description: Current policy + content: + application/json: + schema: {$ref: '#/components/schemas/AuthPolicy'} + '403': {$ref: '#/components/responses/Forbidden'} + put: + operationId: putAuthPolicy + summary: Replace the workspace authentication policy + x-required-permission: system:auth_policy_write + requestBody: + required: true + content: + application/json: + schema: {$ref: '#/components/schemas/AuthPolicyUpdateRequest'} + responses: + '200': + description: Policy updated + content: + application/json: + schema: {$ref: '#/components/schemas/AuthPolicy'} + '400': {$ref: '#/components/responses/BadRequest'} + '403': {$ref: '#/components/responses/Forbidden'} + components: responses: BadRequest: @@ -3142,6 +3173,12 @@ components: access_token: {type: string} refresh_token: {type: string} user: {$ref: '#/components/schemas/AuthMeResponse'} + mfa_enrollment_required: + type: boolean + description: >- + True when workspace policy requires MFA but the user has not + enrolled. The session is still issued (soft enforcement); the + client must force MFA enrollment before anything else. AuthRefreshRequest: type: object @@ -3318,6 +3355,34 @@ components: token: {type: string, description: The raw secret. Shown once.} api_token: {$ref: '#/components/schemas/ApiToken'} + AuthPolicy: + type: object + required: [require_mfa, session_idle_timeout_seconds, session_absolute_timeout_seconds, updated_at] + description: Workspace-wide authentication policy. + properties: + require_mfa: + type: boolean + description: When true, every user must have MFA enrolled (soft-enforced at login). + session_idle_timeout_seconds: + type: integer + description: Inactivity window. A session expires this many seconds after its last use. + session_absolute_timeout_seconds: + type: integer + description: Absolute lifetime cap regardless of activity, in seconds. + updated_at: {type: string, format: date-time} + updated_by: {type: string, format: uuid, nullable: true} + + AuthPolicyUpdateRequest: + type: object + required: [require_mfa, session_idle_timeout_seconds, session_absolute_timeout_seconds] + description: >- + Replaces the whole policy. Bounds (enforced server-side): idle + 300..86400 s, absolute 3600..2592000 s, absolute >= idle. + properties: + require_mfa: {type: boolean} + session_idle_timeout_seconds: {type: integer, minimum: 300, maximum: 86400} + session_absolute_timeout_seconds: {type: integer, minimum: 3600, maximum: 2592000} + UserCreateRequest: type: object required: [username, email, password] diff --git a/audit/events.yaml b/audit/events.yaml index 350937fb..c314c932 100644 --- a/audit/events.yaml +++ b/audit/events.yaml @@ -153,6 +153,10 @@ events: severity: warning description: API key invalidated + - code: auth.policy.updated + severity: warning + description: Workspace authentication policy changed (require-MFA, session timeouts) + # ========================================================================= # authz — RBAC and authorization # ========================================================================= diff --git a/auth/permissions.yaml b/auth/permissions.yaml index a1d3a27e..5d390899 100644 --- a/auth/permissions.yaml +++ b/auth/permissions.yaml @@ -362,6 +362,15 @@ permissions: description: Modify runtime system configuration dangerous: true + - id: system:auth_policy_read + category: system + description: View the workspace authentication policy (require-MFA, session timeouts) + + - id: system:auth_policy_write + category: system + description: Modify the workspace authentication policy (require-MFA, session timeouts) + dangerous: true + # ========================================================================= # role - role administration # ========================================================================= @@ -532,6 +541,8 @@ roles: - integration:* - audit:* - system:read + - system:auth_policy_read + - system:auth_policy_write - id: admin description: Full system administration diff --git a/frontend/src/api/auth-bootstrap.ts b/frontend/src/api/auth-bootstrap.ts index 7b026b63..f1edd7b8 100644 --- a/frontend/src/api/auth-bootstrap.ts +++ b/frontend/src/api/auth-bootstrap.ts @@ -34,6 +34,8 @@ function permissionsForRole(role: string): string[] { 'token:read', 'token:write', 'token:delete', + 'system:auth_policy_read', + 'system:auth_policy_write', 'admin', ]; } @@ -78,6 +80,8 @@ export async function bootstrapAuth(): Promise { 'token:read', 'token:write', 'token:delete', + 'system:auth_policy_read', + 'system:auth_policy_write', 'admin', ], mfaEnabled: false, diff --git a/frontend/src/api/schema.d.ts b/frontend/src/api/schema.d.ts index e42d0a8c..d7331b29 100644 --- a/frontend/src/api/schema.d.ts +++ b/frontend/src/api/schema.d.ts @@ -2022,6 +2022,24 @@ export interface paths { patch?: never; trace?: never; }; + "/api/v1/auth-policy": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** View the workspace authentication policy */ + get: operations["getAuthPolicy"]; + /** Replace the workspace authentication policy */ + put: operations["putAuthPolicy"]; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -2035,6 +2053,8 @@ export interface components { access_token: string; refresh_token: string; user: components["schemas"]["AuthMeResponse"]; + /** @description True when workspace policy requires MFA but the user has not enrolled. The session is still issued (soft enforcement); the client must force MFA enrollment before anything else. */ + mfa_enrollment_required?: boolean; }; AuthRefreshRequest: { refresh_token: string; @@ -2172,6 +2192,25 @@ export interface components { token: string; api_token: components["schemas"]["ApiToken"]; }; + /** @description Workspace-wide authentication policy. */ + AuthPolicy: { + /** @description When true, every user must have MFA enrolled (soft-enforced at login). */ + require_mfa: boolean; + /** @description Inactivity window. A session expires this many seconds after its last use. */ + session_idle_timeout_seconds: number; + /** @description Absolute lifetime cap regardless of activity, in seconds. */ + session_absolute_timeout_seconds: number; + /** Format: date-time */ + updated_at: string; + /** Format: uuid */ + updated_by?: string | null; + }; + /** @description Replaces the whole policy. Bounds (enforced server-side): idle 300..86400 s, absolute 3600..2592000 s, absolute >= idle. */ + AuthPolicyUpdateRequest: { + require_mfa: boolean; + session_idle_timeout_seconds: number; + session_absolute_timeout_seconds: number; + }; UserCreateRequest: { username: string; email: string; @@ -7638,4 +7677,51 @@ export interface operations { 403: components["responses"]["Forbidden"]; }; }; + getAuthPolicy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current policy */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthPolicy"]; + }; + }; + 403: components["responses"]["Forbidden"]; + }; + }; + putAuthPolicy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["AuthPolicyUpdateRequest"]; + }; + }; + responses: { + /** @description Policy updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AuthPolicy"]; + }; + }; + 400: components["responses"]["BadRequest"]; + 403: components["responses"]["Forbidden"]; + }; + }; } diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index cc461b6d..328de3a7 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -100,11 +100,21 @@ export function LoginPage() { }; if (mfaRequired && values.otp) body.otp = values.otp; - const { response, error } = await api.POST('/api/v1/auth/login', { + const { + data: loginData, + response, + error, + } = await api.POST('/api/v1/auth/login', { body, }); if (response.ok) { + // Soft require-MFA enforcement: the session is issued, but workspace + // policy requires this user to enroll in MFA before doing anything + // else. Land them on the profile page where enrollment lives. + const enrollmentRequired = !!( + loginData as { mfa_enrollment_required?: boolean } | undefined + )?.mfa_enrollment_required; // IMPORTANT (C-02): we do NOT read access_token / refresh_token // from the response body. Session cookie is the credential. const { data: me } = await api.GET('/api/v1/auth/me'); @@ -138,6 +148,8 @@ export function LoginPage() { 'token:read', 'token:write', 'token:delete', + 'system:auth_policy_read', + 'system:auth_policy_write', 'admin', ] : ['host:read', 'scan:read'], @@ -145,6 +157,10 @@ export function LoginPage() { }; setIdentity(nextIdentity); } + if (enrollmentRequired) { + navigate({ to: '/settings/profile' }); + return; + } const dest = safeReturnTo(search.return_to); navigate({ to: dest }); return; diff --git a/frontend/src/pages/settings/SecurityPage.tsx b/frontend/src/pages/settings/SecurityPage.tsx index e31323cf..59319a40 100644 --- a/frontend/src/pages/settings/SecurityPage.tsx +++ b/frontend/src/pages/settings/SecurityPage.tsx @@ -10,9 +10,13 @@ import { PageHead, Section, SettingCard, + SettingRow, + FirstSettingRow, BackendPendingBanner, Btn, StatusPill, + Toggle, + Stepper, Modal, FormField, Select, @@ -23,14 +27,16 @@ import type { components } from '@/api/schema'; type ApiToken = components['schemas']['ApiToken']; type RoleEntry = components['schemas']['RoleEntry']; +type AuthPolicy = components['schemas']['AuthPolicy']; // Settings -> Security & auth. // // API tokens (token:* ) are live: service-account tokens for automation, -// shown once at creation. SSO (OIDC/SAML) and authentication policy remain -// backend-pending stubs. +// shown once at creation. Authentication policy (require-MFA + session +// timeouts) is live via system:auth_policy_*. SSO (OIDC/SAML) remains a +// backend-pending stub. // -// Spec: frontend-settings v1.6.0, api-tokens. +// Spec: frontend-settings v1.7.0, api-tokens, api-auth-policy. const inputStyle = { background: 'var(--ow-bg-2)', @@ -64,6 +70,8 @@ export function SecurityPage() { const canRead = useAuthStore((s) => s.hasPermission)('token:read'); const canWrite = useAuthStore((s) => s.hasPermission)('token:write'); const canDelete = useAuthStore((s) => s.hasPermission)('token:delete'); + const canReadPolicy = useAuthStore((s) => s.hasPermission)('system:auth_policy_read'); + const canWritePolicy = useAuthStore((s) => s.hasPermission)('system:auth_policy_write'); const [addOpen, setAddOpen] = useState(false); useEffect(() => { setCrumbs([{ label: 'Settings' }, { label: 'Security & auth' }]); @@ -93,16 +101,19 @@ export function SecurityPage() {
- + {canReadPolicy ? ( + + ) : ( + + You do not have permission to view the authentication policy. + + )}
@@ -139,6 +150,137 @@ export function SecurityPage() { const pad = { padding: 20, color: 'var(--ow-fg-2)', fontSize: 13 } as const; +// AuthPolicySection — live require-MFA + session-timeout policy. +// Timeouts are stored in seconds; presented as minutes (idle) and hours +// (absolute). Bounds mirror the server (idle 5m..24h, absolute 1h..30d). +// Spec: api-auth-policy, frontend-settings. +function AuthPolicySection({ canWrite }: { canWrite: boolean }) { + const queryClient = useQueryClient(); + const [requireMfa, setRequireMfa] = useState(false); + const [idleMin, setIdleMin] = useState(15); + const [absHrs, setAbsHrs] = useState(12); + const [saveError, setSaveError] = useState(null); + + const policyQuery = useQuery({ + queryKey: ['auth-policy'], + queryFn: async () => { + const { data, error, response } = await api.GET('/api/v1/auth-policy'); + if (error || !response.ok) throw new Error(apiErrorMessage(error, 'Failed to load policy')); + return data!; + }, + }); + + // Seed local state once the policy loads. + useEffect(() => { + const p = policyQuery.data; + if (!p) return; + setRequireMfa(p.require_mfa); + setIdleMin(Math.round(p.session_idle_timeout_seconds / 60)); + setAbsHrs(Math.round(p.session_absolute_timeout_seconds / 3600)); + }, [policyQuery.data]); + + const saveMutation = useMutation({ + mutationFn: async () => { + const body: AuthPolicy = { + require_mfa: requireMfa, + session_idle_timeout_seconds: idleMin * 60, + session_absolute_timeout_seconds: absHrs * 3600, + updated_at: new Date().toISOString(), + }; + const { data, response, error } = await api.PUT('/api/v1/auth-policy', { + body: body as never, + }); + if (!response.ok || !data) throw new Error(apiErrorMessage(error, 'Save failed')); + return data; + }, + onSuccess: () => queryClient.invalidateQueries({ queryKey: ['auth-policy'] }), + onError: (e: Error) => setSaveError(e.message), + }); + + if (policyQuery.isPending) return
Loading policy.
; + if (policyQuery.isError) { + return ( +
+ Failed to load authentication policy. {apiErrorMessage(policyQuery.error, '')} +
+ ); + } + + const p = policyQuery.data; + const dirty = + p.require_mfa !== requireMfa || + Math.round(p.session_idle_timeout_seconds / 60) !== idleMin || + Math.round(p.session_absolute_timeout_seconds / 3600) !== absHrs; + + return ( + <> + + + } + /> + + } + /> + + } + /> + + {saveError && ( +
+ {saveError} +
+ )} + {canWrite && ( +
+ { + setSaveError(null); + saveMutation.mutate(); + }} + > + {saveMutation.isPending ? 'Saving.' : 'Save policy'} + +
+ )} + + ); +} + function TokenRow({ token, isFirst, diff --git a/frontend/tests/pages/settings.test.ts b/frontend/tests/pages/settings.test.ts index 9cdb4cf8..e00286ac 100644 --- a/frontend/tests/pages/settings.test.ts +++ b/frontend/tests/pages/settings.test.ts @@ -63,6 +63,7 @@ const NOTIF_SRC = readFileSync( 'utf8', ); const SEC_SRC = readFileSync(resolve(process.cwd(), 'src/pages/settings/SecurityPage.tsx'), 'utf8'); +const LOGIN_SRC = readFileSync(resolve(process.cwd(), 'src/pages/LoginPage.tsx'), 'utf8'); const PREFS_STORE_SRC = readFileSync( resolve(process.cwd(), 'src/store/usePreferencesStore.ts'), 'utf8', @@ -392,7 +393,23 @@ describe('frontend-settings — structural', () => { // Write/delete controls gate on their permissions. expect(SEC_SRC).toMatch(/hasPermission\)\('token:write'\)/); expect(SEC_SRC).toMatch(/hasPermission\)\('token:delete'\)/); - // SSO + auth-policy stay pending. + // SSO stays pending (still renders a BackendPendingBanner). expect(SEC_SRC).toContain('BackendPendingBanner'); }); + + test('frontend-settings/AC-25 — Security: live auth-policy section + login enrollment routing', () => { + // Auth-policy section loads + saves the policy, perm-gated. + expect(SEC_SRC).toMatch(/queryKey:\s*\['auth-policy'\]/); + expect(SEC_SRC).toMatch(/api\.GET\(\s*['"]\/api\/v1\/auth-policy['"]/); + expect(SEC_SRC).toMatch(/api\.PUT\(\s*['"]\/api\/v1\/auth-policy['"]/); + expect(SEC_SRC).toMatch(/invalidateQueries\(\{\s*queryKey:\s*\['auth-policy'\]/); + expect(SEC_SRC).toMatch(/hasPermission\)\('system:auth_policy_read'\)/); + expect(SEC_SRC).toMatch(/hasPermission\)\('system:auth_policy_write'\)/); + // require-MFA toggle + timeout steppers. + expect(SEC_SRC).toContain(' Security without a redeploy. Update re-primes the +// identity package's active windows so the change takes effect for newly +// issued sessions and the rolling idle extension immediately. +// +// Spec: system-auth-policy, api-auth-policy. +package authpolicy + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Hanalyx/openwatch/internal/identity" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" +) + +// Bounds for the session timeout windows. Enforced in Update so a typo in +// the UI can neither disable session expiry (a zero/huge window) nor set a +// nonsensical absolute < idle. Locked per spec C-03; widen only via spec. +const ( + MinIdleTimeout = 5 * time.Minute + MaxIdleTimeout = 24 * time.Hour + MinAbsoluteTimeout = 1 * time.Hour + MaxAbsoluteTimeout = 30 * 24 * time.Hour +) + +// ErrInvalidParams is returned when Update validation fails (out-of-bounds +// window, or absolute < idle). +var ErrInvalidParams = errors.New("authpolicy: invalid parameters") + +// Policy is the effective authentication policy. +type Policy struct { + RequireMFA bool + IdleTimeout time.Duration + AbsoluteTimeout time.Duration + UpdatedAt time.Time + UpdatedBy *uuid.UUID +} + +// UpdateParams is the input to Update. All fields are required — the PUT +// replaces the whole policy (there is exactly one). +type UpdateParams struct { + RequireMFA bool + IdleTimeout time.Duration + AbsoluteTimeout time.Duration + UpdatedBy *uuid.UUID +} + +// Service is the auth-policy store. +type Service struct { + pool *pgxpool.Pool +} + +// NewService binds a Service to a DB pool. +func NewService(pool *pgxpool.Pool) *Service { + return &Service{pool: pool} +} + +// Get returns the current policy. The singleton row is seeded by the +// migration, so a missing row is an internal error rather than a normal +// empty state. +// +// Spec api-auth-policy AC-01. +func (s *Service) Get(ctx context.Context) (Policy, error) { + const stmt = ` + SELECT require_mfa, session_idle_timeout_seconds, + session_absolute_timeout_seconds, updated_at, updated_by + FROM auth_policy + WHERE id = TRUE` + var ( + p Policy + idleSecs int + absSecs int + updatedBy *uuid.UUID + updatedRow time.Time + ) + if err := s.pool.QueryRow(ctx, stmt).Scan( + &p.RequireMFA, &idleSecs, &absSecs, &updatedRow, &updatedBy, + ); err != nil { + return Policy{}, fmt.Errorf("authpolicy: get: %w", err) + } + p.IdleTimeout = time.Duration(idleSecs) * time.Second + p.AbsoluteTimeout = time.Duration(absSecs) * time.Second + p.UpdatedAt = updatedRow + p.UpdatedBy = updatedBy + return p, nil +} + +// Update validates and persists the policy, then re-primes the identity +// package's active session windows so the change takes effect at once. +// +// Spec api-auth-policy AC-02, system-auth-policy AC-05. +func (s *Service) Update(ctx context.Context, p UpdateParams) (Policy, error) { + if err := validate(p); err != nil { + return Policy{}, err + } + const stmt = ` + UPDATE auth_policy + SET require_mfa = $1, + session_idle_timeout_seconds = $2, + session_absolute_timeout_seconds = $3, + updated_at = now(), + updated_by = $4 + WHERE id = TRUE + RETURNING require_mfa, session_idle_timeout_seconds, + session_absolute_timeout_seconds, updated_at, updated_by` + var ( + out Policy + idleSecs int + absSecs int + updatedBy *uuid.UUID + updatedAt time.Time + ) + if err := s.pool.QueryRow(ctx, stmt, + p.RequireMFA, + int(p.IdleTimeout.Seconds()), + int(p.AbsoluteTimeout.Seconds()), + p.UpdatedBy, + ).Scan(&out.RequireMFA, &idleSecs, &absSecs, &updatedAt, &updatedBy); err != nil { + return Policy{}, fmt.Errorf("authpolicy: update: %w", err) + } + out.IdleTimeout = time.Duration(idleSecs) * time.Second + out.AbsoluteTimeout = time.Duration(absSecs) * time.Second + out.UpdatedAt = updatedAt + out.UpdatedBy = updatedBy + s.prime(out) + return out, nil +} + +// Prime loads the persisted policy and installs its windows into the +// identity package. Called once at server startup so sessions honour the +// stored policy from the first request, not just after the first Update. +// +// Spec system-auth-policy AC-06. +func (s *Service) Prime(ctx context.Context) error { + p, err := s.Get(ctx) + if err != nil { + return err + } + s.prime(p) + return nil +} + +// prime installs the policy's windows into the identity package. +func (s *Service) prime(p Policy) { + identity.SetSessionWindows(identity.Windows{ + Idle: p.IdleTimeout, + Absolute: p.AbsoluteTimeout, + }) +} + +// validate enforces the window bounds and the absolute >= idle invariant. +func validate(p UpdateParams) error { + if p.IdleTimeout < MinIdleTimeout || p.IdleTimeout > MaxIdleTimeout { + return fmt.Errorf("%w: idle timeout must be between %s and %s", + ErrInvalidParams, MinIdleTimeout, MaxIdleTimeout) + } + if p.AbsoluteTimeout < MinAbsoluteTimeout || p.AbsoluteTimeout > MaxAbsoluteTimeout { + return fmt.Errorf("%w: absolute timeout must be between %s and %s", + ErrInvalidParams, MinAbsoluteTimeout, MaxAbsoluteTimeout) + } + if p.AbsoluteTimeout < p.IdleTimeout { + return fmt.Errorf("%w: absolute timeout cannot be shorter than idle timeout", + ErrInvalidParams) + } + return nil +} diff --git a/internal/authpolicy/authpolicy_test.go b/internal/authpolicy/authpolicy_test.go new file mode 100644 index 00000000..6fdea771 --- /dev/null +++ b/internal/authpolicy/authpolicy_test.go @@ -0,0 +1,219 @@ +// @spec system-auth-policy +// +// Singleton store + window-bounds validation + identity-window priming. +// DSN-gated via OPENWATCH_TEST_DSN. The source-inspection ACs run without +// a database. + +package authpolicy + +import ( + "context" + "os" + "strings" + "testing" + "time" + + "github.com/Hanalyx/openwatch/internal/db" + "github.com/Hanalyx/openwatch/internal/db/migrations" + "github.com/Hanalyx/openwatch/internal/identity" + "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgxpool" +) + +func freshService(t *testing.T) (*Service, *pgxpool.Pool) { + t.Helper() + dsn := os.Getenv("OPENWATCH_TEST_DSN") + if dsn == "" { + t.Skip("set OPENWATCH_TEST_DSN to run authpolicy tests") + } + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + t.Cleanup(cancel) + pool, err := db.NewPool(ctx, dsn, 5) + if err != nil { + t.Fatalf("NewPool: %v", err) + } + t.Cleanup(pool.Close) + if err := migrations.Apply(ctx, pool); err != nil { + t.Fatalf("migrations.Apply: %v", err) + } + // Reset the singleton to its seeded defaults so tests start clean. Use + // INSERT…ON CONFLICT because a server-package fixture's TRUNCATE users + // CASCADE (shared test DB) can have removed the seeded row. + _, _ = pool.Exec(ctx, `INSERT INTO auth_policy (id) VALUES (true) + ON CONFLICT (id) DO UPDATE + SET require_mfa = false, session_idle_timeout_seconds = 900, + session_absolute_timeout_seconds = 43200, updated_by = NULL`) + return NewService(pool), pool +} + +// @ac AC-01 +func TestGet_SeededDefaultsAndSingleton(t *testing.T) { + t.Run("system-auth-policy/AC-01", func(t *testing.T) { + svc, pool := freshService(t) + ctx := context.Background() + p, err := svc.Get(ctx) + if err != nil { + t.Fatalf("Get: %v", err) + } + if p.RequireMFA { + t.Errorf("default RequireMFA = true, want false") + } + if p.IdleTimeout != 15*time.Minute || p.AbsoluteTimeout != 12*time.Hour { + t.Errorf("defaults = (%s,%s), want (15m,12h)", p.IdleTimeout, p.AbsoluteTimeout) + } + // Singleton: a second row insert is rejected by the CHECK. + if _, err := pool.Exec(ctx, `INSERT INTO auth_policy (id) VALUES (false)`); err == nil { + t.Error("inserting a second auth_policy row succeeded; singleton CHECK not enforced") + } + }) +} + +// @ac AC-02 +func TestUpdate_PersistsAndValidates(t *testing.T) { + t.Run("system-auth-policy/AC-02", func(t *testing.T) { + svc, _ := freshService(t) + ctx := context.Background() + out, err := svc.Update(ctx, UpdateParams{ + RequireMFA: true, + IdleTimeout: 30 * time.Minute, + AbsoluteTimeout: 24 * time.Hour, + }) + if err != nil { + t.Fatalf("Update valid: %v", err) + } + if !out.RequireMFA || out.IdleTimeout != 30*time.Minute || out.AbsoluteTimeout != 24*time.Hour { + t.Errorf("update echo = %+v", out) + } + // Persisted. + got, _ := svc.Get(ctx) + if !got.RequireMFA || got.IdleTimeout != 30*time.Minute { + t.Errorf("get after update = %+v, want persisted", got) + } + // Out-of-bounds idle (below 5m). + if _, err := svc.Update(ctx, UpdateParams{IdleTimeout: time.Minute, AbsoluteTimeout: 12 * time.Hour}); err == nil { + t.Error("idle below floor accepted, want ErrInvalidParams") + } + // Out-of-bounds absolute (above 30d). + if _, err := svc.Update(ctx, UpdateParams{IdleTimeout: 15 * time.Minute, AbsoluteTimeout: 60 * 24 * time.Hour}); err == nil { + t.Error("absolute above ceiling accepted, want ErrInvalidParams") + } + // Absolute shorter than idle. + if _, err := svc.Update(ctx, UpdateParams{IdleTimeout: 2 * time.Hour, AbsoluteTimeout: time.Hour}); err == nil { + t.Error("absolute 46*time.Minute { + t.Errorf("session idle window = %s, want ~45m", idle) + } + if abs < 19*time.Hour+50*time.Minute || abs > 20*time.Hour+10*time.Minute { + t.Errorf("session absolute window = %s, want ~20h", abs) + } + + // Restore defaults so we don't leak windows into other packages' + // expectations within the same test process. + identity.SetSessionWindows(identity.Windows{ + Idle: identity.DefaultSessionInactivityWindow, + Absolute: identity.DefaultSessionAbsoluteWindow, + }) + }) +} + +// @ac AC-03 +func TestSoftMFA_LoginSourceInspection(t *testing.T) { + t.Run("system-auth-policy/AC-03", func(t *testing.T) { + raw, err := os.ReadFile("../server/auth_handlers.go") + if err != nil { + t.Fatalf("read source: %v", err) + } + src := string(raw) + if !strings.Contains(src, "mfaEnrollmentRequired") { + t.Error("login does not compute mfaEnrollmentRequired") + } + // The flag is gated on RequireMFA AND not-enrolled. + if !strings.Contains(src, "pol.RequireMFA") || !strings.Contains(src, "!enrolled") { + t.Error("mfaEnrollmentRequired not gated on RequireMFA && !enrolled") + } + }) +} + +// @ac AC-04 +func TestSoftMFA_DoesNotBlock(t *testing.T) { + t.Run("system-auth-policy/AC-04", func(t *testing.T) { + raw, err := os.ReadFile("../server/auth_handlers.go") + if err != nil { + t.Fatalf("read source: %v", err) + } + src := string(raw) + // The require-MFA soft branch must sit AFTER session issuance and + // must not early-return — the session/cookies are still set. + issueIdx := strings.Index(src, "IssueSession") + flagIdx := strings.Index(src, "mfaEnrollmentRequired := false") + if issueIdx < 0 || flagIdx < 0 { + t.Fatal("expected IssueSession and the soft-MFA branch in the login handler") + } + // The flag computation precedes token issuance (it is evaluated + // before IssueSession) but does not return; assert no writeError in + // the soft branch by checking the branch body. + branch := src[flagIdx:] + end := strings.Index(branch, "\n\t}\n") + if end > 0 { + branch = branch[:end] + } + if strings.Contains(branch, "writeError") || strings.Contains(branch, "return") { + t.Error("soft require-MFA branch blocks login (contains writeError/return)") + } + }) +} + +// @ac AC-06 +func TestPrime_WiredInServerNew(t *testing.T) { + t.Run("system-auth-policy/AC-06", func(t *testing.T) { + raw, err := os.ReadFile("../server/server.go") + if err != nil { + t.Fatalf("read source: %v", err) + } + src := string(raw) + if !strings.Contains(src, "authpolicy.NewService") || !strings.Contains(src, ".Prime(") { + t.Error("server.New does not construct the authpolicy service and prime it") + } + if !strings.Contains(src, "prime skipped") { + t.Error("Prime failure is not handled non-fatally (no warn-and-continue)") + } + }) +} diff --git a/internal/db/migrations/0033_auth_policy.sql b/internal/db/migrations/0033_auth_policy.sql new file mode 100644 index 00000000..cd9b8f81 --- /dev/null +++ b/internal/db/migrations/0033_auth_policy.sql @@ -0,0 +1,43 @@ +-- 0033_auth_policy.sql +-- +-- Workspace-wide authentication policy: a single-row table holding the +-- require-MFA flag and the session idle/absolute timeout windows. These +-- were previously hard-coded constants in the identity package; promoting +-- them to data lets a security admin tune them from Settings -> Security +-- without a redeploy. +-- +-- Singleton pattern: a fixed BOOLEAN primary key pinned to TRUE so at most +-- one row can ever exist. Reads and writes always target id = TRUE. +-- +-- Defaults match the historical constants (15-minute idle, 12-hour +-- absolute) so promoting to data is behaviour-preserving until an admin +-- changes it. + +-- +goose Up +CREATE TABLE auth_policy ( + -- Singleton guard: only id = TRUE is permitted, so the table holds at + -- most one row. + id BOOLEAN PRIMARY KEY DEFAULT TRUE + CHECK (id = TRUE), + -- When TRUE, every user must have MFA enrolled. Soft enforcement: a + -- password-valid but non-enrolled user still authenticates, but the + -- login response flags mfa_enrollment_required so the UI forces + -- enrollment before anything else. Avoids locking out users who have + -- no other path to reach the (auth-gated) enrollment endpoint. + require_mfa BOOLEAN NOT NULL DEFAULT FALSE, + -- Inactivity window: a session expires this many seconds after its + -- last use. Bounds (5 min .. 24 h) are enforced in the service layer. + session_idle_timeout_seconds INTEGER NOT NULL DEFAULT 900, + -- Absolute lifetime: a session cannot live longer than this regardless + -- of activity. Bounds (1 h .. 30 d) enforced in the service layer. + session_absolute_timeout_seconds INTEGER NOT NULL DEFAULT 43200, + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + -- The admin who last changed the policy (NULL for the seeded default). + updated_by UUID REFERENCES users(id) ON DELETE SET NULL +); + +-- Seed the singleton row with the behaviour-preserving defaults. +INSERT INTO auth_policy (id) VALUES (TRUE); + +-- +goose Down +DROP TABLE IF EXISTS auth_policy; diff --git a/internal/identity/sessions.go b/internal/identity/sessions.go index 7a7d27c8..82ee7fcf 100644 --- a/internal/identity/sessions.go +++ b/internal/identity/sessions.go @@ -8,6 +8,7 @@ import ( "encoding/base64" "errors" "fmt" + "sync/atomic" "time" "github.com/google/uuid" @@ -25,16 +26,60 @@ var ( ErrSessionExpired = errors.New("identity: session expired") ) -// Inactivity + absolute timeout windows. Locked per spec C-06; do not -// loosen without amending the spec. +// Default inactivity + absolute timeout windows. These are the baseline +// per spec C-06; a security admin may override them via the auth policy +// (Settings -> Security). The defaults preserve the historical behaviour +// (15-minute idle, 12-hour absolute) until the policy changes them. const ( - SessionInactivityWindow = 15 * time.Minute - SessionAbsoluteWindow = 12 * time.Hour + DefaultSessionInactivityWindow = 15 * time.Minute + DefaultSessionAbsoluteWindow = 12 * time.Hour // SessionTokenBytes is the entropy of the presentation token. 32 bytes // (256-bit) per spec C-05. SessionTokenBytes = 32 ) +// Windows carries the active session timeout configuration. The idle +// window bounds inactivity; the absolute window caps total lifetime +// regardless of activity. +// +// Spec system-auth-policy C-02, system-auth-identity C-06. +type Windows struct { + Idle time.Duration + Absolute time.Duration +} + +// sessionWindows holds the active windows, swappable at runtime by the +// auth-policy service. An atomic pointer keeps the read path (every +// session verification) lock-free; the default (nil) preserves the +// historical constants so code paths that never set a policy — and tests +// — behave exactly as before. +var sessionWindows atomic.Pointer[Windows] + +// SetSessionWindows installs the active session timeout windows. Called at +// startup once the auth policy is loaded, and again whenever an admin +// updates the policy. Non-positive fields fall back to the defaults so a +// malformed policy can never disable session expiry. +// +// Spec system-auth-policy AC-05. +func SetSessionWindows(w Windows) { + if w.Idle <= 0 { + w.Idle = DefaultSessionInactivityWindow + } + if w.Absolute <= 0 { + w.Absolute = DefaultSessionAbsoluteWindow + } + sessionWindows.Store(&w) +} + +// CurrentWindows returns the active windows, or the defaults when no +// policy has been installed. +func CurrentWindows() Windows { + if w := sessionWindows.Load(); w != nil { + return *w + } + return Windows{Idle: DefaultSessionInactivityWindow, Absolute: DefaultSessionAbsoluteWindow} +} + // RefreshCookieName is the HttpOnly cookie carrying the refresh token's // presentation form. Set at login and rotated by the refresh-cookie // endpoint. JS cannot read it; only the server consumes it. @@ -74,13 +119,14 @@ func IssueSession(ctx context.Context, pool *pgxpool.Pool, userID uuid.UUID, rem return "", Session{}, fmt.Errorf("identity: uuid: %w", err) } now := time.Now().UTC() + win := CurrentWindows() sess = Session{ ID: id, UserID: userID, CreatedAt: now, LastSeen: now, - ExpiresAt: now.Add(SessionInactivityWindow), - AbsoluteExpiresAt: now.Add(SessionAbsoluteWindow), + ExpiresAt: now.Add(win.Idle), + AbsoluteExpiresAt: now.Add(win.Absolute), RemoteAddr: remoteAddr, UserAgent: userAgent, } @@ -139,7 +185,7 @@ func VerifySession(ctx context.Context, pool *pgxpool.Pool, token string) (Sessi // Touch last_seen and extend expires_at by the inactivity window — // but never beyond absolute_expires_at. - newExpires := now.Add(SessionInactivityWindow) + newExpires := now.Add(CurrentWindows().Idle) if newExpires.After(s.AbsoluteExpiresAt) { newExpires = s.AbsoluteExpiresAt } diff --git a/internal/server/api/server.gen.go b/internal/server/api/server.gen.go index ba6c0d2c..308c520e 100644 --- a/internal/server/api/server.gen.go +++ b/internal/server/api/server.gen.go @@ -1113,9 +1113,12 @@ type AuthLoginRequest struct { // AuthLoginResponse defines model for AuthLoginResponse. type AuthLoginResponse struct { - AccessToken string `json:"access_token"` - RefreshToken string `json:"refresh_token"` - User AuthMeResponse `json:"user"` + AccessToken string `json:"access_token"` + + // MfaEnrollmentRequired True when workspace policy requires MFA but the user has not enrolled. The session is still issued (soft enforcement); the client must force MFA enrollment before anything else. + MfaEnrollmentRequired *bool `json:"mfa_enrollment_required,omitempty"` + RefreshToken string `json:"refresh_token"` + User AuthMeResponse `json:"user"` } // AuthMFAEnrollResponse defines model for AuthMFAEnrollResponse. @@ -1152,6 +1155,27 @@ type AuthPasswordChangeRequest struct { NewPassword string `json:"new_password"` } +// AuthPolicy Workspace-wide authentication policy. +type AuthPolicy struct { + // RequireMfa When true, every user must have MFA enrolled (soft-enforced at login). + RequireMfa bool `json:"require_mfa"` + + // SessionAbsoluteTimeoutSeconds Absolute lifetime cap regardless of activity, in seconds. + SessionAbsoluteTimeoutSeconds int `json:"session_absolute_timeout_seconds"` + + // SessionIdleTimeoutSeconds Inactivity window. A session expires this many seconds after its last use. + SessionIdleTimeoutSeconds int `json:"session_idle_timeout_seconds"` + UpdatedAt time.Time `json:"updated_at"` + UpdatedBy *openapi_types.UUID `json:"updated_by,omitempty"` +} + +// AuthPolicyUpdateRequest Replaces the whole policy. Bounds (enforced server-side): idle 300..86400 s, absolute 3600..2592000 s, absolute >= idle. +type AuthPolicyUpdateRequest struct { + RequireMfa bool `json:"require_mfa"` + SessionAbsoluteTimeoutSeconds int `json:"session_absolute_timeout_seconds"` + SessionIdleTimeoutSeconds int `json:"session_idle_timeout_seconds"` +} + // AuthRefreshRequest defines model for AuthRefreshRequest. type AuthRefreshRequest struct { RefreshToken string `json:"refresh_token"` @@ -2759,6 +2783,9 @@ type PostAlertResolveJSONRequestBody = AlertLifecycleRequest // PostAlertSilenceJSONRequestBody defines body for PostAlertSilence for application/json ContentType. type PostAlertSilenceJSONRequestBody = AlertLifecycleRequest +// PutAuthPolicyJSONRequestBody defines body for PutAuthPolicy for application/json ContentType. +type PutAuthPolicyJSONRequestBody = AuthPolicyUpdateRequest + // PostAuthLoginJSONRequestBody defines body for PostAuthLogin for application/json ContentType. type PostAuthLoginJSONRequestBody = AuthLoginRequest @@ -2899,6 +2926,12 @@ type ServerInterface interface { // List audit events (cursor-paginated, newest first) // (GET /api/v1/audit/events) GetAuditEvents(w http.ResponseWriter, r *http.Request, params GetAuditEventsParams) + // View the workspace authentication policy + // (GET /api/v1/auth-policy) + GetAuthPolicy(w http.ResponseWriter, r *http.Request) + // Replace the workspace authentication policy + // (PUT /api/v1/auth-policy) + PutAuthPolicy(w http.ResponseWriter, r *http.Request) // Username/password login with optional TOTP // (POST /api/v1/auth/login) PostAuthLogin(w http.ResponseWriter, r *http.Request) @@ -3280,6 +3313,18 @@ func (_ Unimplemented) GetAuditEvents(w http.ResponseWriter, r *http.Request, pa w.WriteHeader(http.StatusNotImplemented) } +// View the workspace authentication policy +// (GET /api/v1/auth-policy) +func (_ Unimplemented) GetAuthPolicy(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + +// Replace the workspace authentication policy +// (PUT /api/v1/auth-policy) +func (_ Unimplemented) PutAuthPolicy(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotImplemented) +} + // Username/password login with optional TOTP // (POST /api/v1/auth/login) func (_ Unimplemented) PostAuthLogin(w http.ResponseWriter, r *http.Request) { @@ -4410,6 +4455,34 @@ func (siw *ServerInterfaceWrapper) GetAuditEvents(w http.ResponseWriter, r *http handler.ServeHTTP(w, r) } +// GetAuthPolicy operation middleware +func (siw *ServerInterfaceWrapper) GetAuthPolicy(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetAuthPolicy(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// PutAuthPolicy operation middleware +func (siw *ServerInterfaceWrapper) PutAuthPolicy(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.PutAuthPolicy(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // PostAuthLogin operation middleware func (siw *ServerInterfaceWrapper) PostAuthLogin(w http.ResponseWriter, r *http.Request) { @@ -7286,6 +7359,12 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/audit/events", wrapper.GetAuditEvents) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/auth-policy", wrapper.GetAuthPolicy) + }) + r.Group(func(r chi.Router) { + r.Put(options.BaseURL+"/api/v1/auth-policy", wrapper.PutAuthPolicy) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/auth/login", wrapper.PostAuthLogin) }) @@ -7610,406 +7689,414 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "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", - "v37y5PHgt59/+/8DAAD//w==", + "7L1pkxs3tiD6V05wJsLkE8kqrWNLobghl6RrdWu7VbL7zWv6scFMkIQrCWQDyKLYbb+YT/MDbswvvL/k", + "BQ6A3IhMJlmr3fWlWy4isRycHWf5Zy8Sq1RwyrXqPf9nT1KVCq4o/sf3JD6lf8+o0ua/IsE15fhPkqYJ", + "i4hmgh/9ogQ3f1PRkq6I+dd/l3Tee977b0fF1Ef2V3X0Rkoh3/ALmoiU9n777bdhL6Yqkiw1k/We934i", + "CYtx5iGkZMG4+7eQwGK6SoWmPNoANfP0fhv23go5Y3FM+c1t8YQkCZWQkOhcgV5SkPTvGZM0hpTKFVPK", + "DPtt2PtA9VLEH4V+lSRiTeOb2+FfpOAL+OHLl8+wwk2Y7XwU+q3I+A1u45QqkcmIAhca5rj2b8PeGZUX", + "LKI/cnJBWEJmCb25HX1PonPGF6DsHnBja7w6wfEqzQ9U9syXblKz5qtIswumN+bfqRQplZpZElkKpacM", + "YToXckV073kvy1jcG/Z4lrjTaZnRYU9vUtp73lNaMr4wgAh/tjVMRFEmJY2nRFfGx0TTkWYrGvpI0Qsq", + "3Y4pz1a953/tMT4XvWEvEevesLeiMctWvWFvyRbL3rAXSaZZRJLez6HZ8BrLc5GESm0WloQrEiF4hz3G", + "NU0StqA8MrsiWczMoJXgTAucLDh7tloRiVvd+k0zbfGj9stvw56nOjyagZzbZenw/vsqEIs9iNkvNNJm", + "HX/Dn8mCBm4ZOcw0EpnF0BXjbGUAcZxPZY6+oMiSmKYr/Cz/RxvO5rj1Wz4XkZLgf3P6VU+jTCohzTQ7", + "MKoOE1x9WN188OzxivFTkVB16rj/NgSk+bnzmcxkb7iWgUPVNmnnDe4KMWxrIyQ652Kd0HjRThE7qa8y", + "0WxzEAUjFUztnwPIOxNxGKsjSYnek6JjGmfp9JyGZ4yZMpLnkjApZjkQIHeVHUqqRHJxSejkkxwIHJkl", + "1AHnuvk1SwwLPnin+fcZ1yw5HGJKE10VG4bZoWgoUV+vWLBXQLlXQsfgITVZWI4Qx8zIH5J8rnCK7Q/q", + "LKZJuAx7WRrvSaAhgVSQbIVVBAWUBVUHQWXmec/mNNpECS2p51VF51NqQQKGB8FcSPj86ewLJP5DoDxO", + "BeNajcEBH0gU0VQrIByE/xwR4AWQJHE/AwFJiRIcJzUqE0p5iKkmLBn3hnXBgYNRaJKv7ylf6GXv+aOn", + "zwIXeilk+60JVios0vcU0iiMrkdCB285ZV/EuTVqqhf76vM70OYno9mTmGgyhi+ouEaSamAKPr756c0p", + "MB4lWUzj7Rs5RPjQrymTVF2KfXbk7wlReppdVpJxsgpTdirpnH3dhutHwUcOhjFTaUI2YIdCn44XYxDr", + "8yl5OHsUPY6fDMIi5kKcX1bCiFxCVHdnbtj8COulULRkZ1oD1CJERKQ0d9yJNyGAcnAUS1eUkzbUPMFh", + "JQZUxbKrwBh/iXXOsWLc//fDfaBo1NISvEhkmJ2C/ipTGmYUCBihxBHSg51gdBD0q+2GVdO9krXbEFOg", + "lmYDnz6evBkCMyYpU2AvBLxrBgRPNmM400JSYBq4WL8w/x8RbszZmRmpJaMXNAayIIxvswCSsqn2/KWV", + "73k+ZFhfmCH5I1jiGcMZnkDwiI53gtBOOSztpw2K71kI0fC7PTh5+URtlombN7ghI/HeXDh3Rd0+sXD5", + "Z8ji0EI6xOxgnpjBjaZFJKSkCfpImlRKK5GbNaTKwsXhrlcNj4SM9/4otkCtXnKTepeLZum8T10hno/3", + "QN+t3paU9g4st3Znw17uNylddgcNMMe+q1FtCmTerd/UlUzy94yC/fkFpEQhP/03+4eXoAXMqY6WyHPN", + "TJCaDQ8PdGSU9xIGjF6+FwvGG4WS0GlNnlSlSUgrNadaCxkfIIgyReVBMqwGgHye0m52AKDJj2OUeKUK", + "5r+159WcTCmXIklWlOtpsY0tvi8zo45QDmshz1VKIgqpSFi08f5wBR/evoJZpvH+zSFgSRT6XO0KNPa6", + "K+oyKP80SxJgSmU0hr4SczN2LmREzXYGL3CqKGGUa0Cxjb/hQsWuYUbnRjgSvtFLxhdAE1WWRjMhEkq4", + "Jfq5pGrZAhCz7900pJcfaA71+v1VgF5f063QdJ8f3r56gwdrvtNUigtmAMj4YppJtpsdbX3RsvpPVLL5", + "5gpJqrYXM0Hj8vRzoek2A4DFlOuge75BNDI1JVzwzUpkZVlSxgvR2e2MQ2tzhg5UUtr3EWRbS7qzVids", + "hmAz2OjK6QeHmmoNQKoyvg4QLPE3uyc3ddOhPjsmeLIkfNFsgqAY5Xpa4eDtHJvTdffhtaNsLVebrvE0", + "yDS3GexfPFsdrVlMgWR6aa7ePow5Vhvyt+COpqs5CUxp2DWKXDCqy8YyZeSjS3JRZqOe+44c942BaEiM", + "bBmEGanj4VMyUyLJNJ0adU5keqpoJHisAp4MNxKdUmY0RCQFSRdExglVCsQciHscQWPIzVRav/To4tdn", + "cdJh7XfczwxrxmOxHsOrXAw509UaXyvCN35lIHNNJTCtICFKG+iFN7O/+7D45iCXbf1VpYQEO0DT4eYq", + "52nH4h9xYKNX8pSmCYmodVqsl8Ycd3gM34vMQLif45t9iR0pFtPBczB7h8fHx+Pxt8+eHB+DGoLfLzx+", + "Zv7+6Ol3j45rv0yy4+PH9CV+vZNUDsPpFflqnwLd+sPicdDs6xBUzafEo5YnDM13rVffdNunVoVpZL27", + "1KqtTZeHhxY9IZouhNzYV8Wt9Spo1ijQOrnsi4mC+xCcU8c6vpeUnMdizQOix7/ObJFAxiUl0dLQMjyA", + "yEjmKNPsgk7nhCWZpAqRNnrcGxZMgHH97EmQ08R0IUkc0s87LUNfPuy4jjtmdY2WeTvunxs5NE2lmIXO", + "wAXgi2LCLig3IkGKNdCvTGnVbXrBE8bp/sB5edxl/romaxcrXUrpmc6BsHbiXSh2sqTR+SlVWRJysUpZ", + "chFVD6ij1BPz0JwQZxR8Kuk8UzQewoxwTuV0xdSK6GiJAVbmI5z0BRh5A4KDytB26eStpeupgytLmN5M", + "lSY6Cwjfz0Lp0XKjNJVUoc1nxkFfkRWFC5Jk1kmbUslEzCJIhEhhLbIkhrVkGl2z/kExv0gjpXj1v9CZ", + "G3w6tNDf0wvlpw7bC07LRqBPV4FTHxtwOvzqdWDjxUm25i4foBnuO3FL8DlbNPMuIwQCl2dWBrNteUES", + "fAQsszRDrgpmNBFr62lfGsYukhj6z7zwHozhNZ2TLNHw8NEx9B+tzI02Sr1nx22Mr/Mu63tcM71sZYzh", + "HT8+Pob+04N2LNa8827tHok2ZElm4oJ2gua3ZnOPjw/Z3YqY/+CER3S6SMQsJLtK5gMioKVMzaJzhW4e", + "/KMCx7VV0E4orxMGBpWKKU1jw6LiEFQYh9IsDdf0zIDi2fFqMIaPQsOGamNBiREGF9L4BWhJonMa58/Y", + "KZUjM39lbpUwjCjbE5hWElw9ar48Dp/2O3PYhwdhpSSaThO2YgF1/QP5arbhDFs4O/uhJEoUxJlhjjBP", + "KNWg1pSmCvoPx+NHx8eVjTyqbONhaBclvTOIESOLb19OPo+s4AL3RckuxLUfV5d+vHPlEvOa5uS1vYeT", + "4jY8E1fezZhP8F//+z/LvNDs52F1Pw937CeoUSBUahxvWGXTJe6yTWJV8DYduYIKQX7QTaA0+5uiXOC0", + "+VIDIgrZPWK6OuTrup/G/rk0566DneWKTPVAGKqAmOlUiRr5GG4iaWRoB0eNlCZSI+o6DQtDXpD9zJlU", + "GnlpWfPc67m8fGUu0Gp7T8zodyqnY3O8qYXIOHDlIQ5uD5zHwnbQwu0XjmwO+NIpoXt9ifFUUwxPxrfP", + "PT7edpbn5w3vKHzCxj0EbyqIg5Kit5ckJ4ngzY5OpqYOl0M8XJ67MAIzBxDrfFFG/V6B+wz6gicbo3uz", + "2L7pqEik9KUdNQiigffvVpdzEkmBFvCNtWR9DgBZUataQR+3MvjGLiVWTGs0lvZ8XsM9lqML7XZ7Ngw1", + "HBhpPgnHhvgcjtLxcZrhJX1xdpc7rrc9loZkejl1iRzl46qlCy4suZxnQi+DR6+5SEqwfnj86Mkw+EhS", + "wqpmBNjz1sru9YBhxi4MzTSFOZd+R8d6upREhd8gLokde8fMXtVjr913HqBVehspo0E7Pr1nSrfI4Xxc", + "9yiBYu7ilXPHS1V5mfbttjxXXwXmHxLw6L8Je+N3EdehD2pMlQT3NsV1pchbpwV3P9MZ0+UnzrJ4diMi", + "sVq5OKrGWeaML6hMJdsxrjFY6qDnmP2eMLtRbeUKy9cdpI9MabE6FQndIR6aOXsXfmyvOCVaU2kE4f/7", + "VzL6x8/mf45H301//ufD4bPHv/33EIg6P2ivGH9nf3y483W75oPf/cpdgKmZjRz0PID3M8tYoqeMhwnu", + "qp70tw5dXnk3CF4zFYkLKhtdejHVNNJTwadoZhiFX5NIt3p4MF/giKTs6OLhkXNHZVqMKP97RjOqgKC3", + "Yhz7xeEXMcu9KZyu8efCRtaZ5Iwv4NHxwzHgOnOSKDrMh6IjeANE2/8aCzX1cyPtwscf378vWUxGNMVZ", + "QiWkzGXiriBL0WPGwRyfaCEhSvDXUzqSGYd8t0Gd1ntlmv0QeKaIxJgw8V//6/84KHyjiplhRiOxogri", + "jGIcLHCx7g9g1HQu+jWiNA68N4/d0+az4yffHh8X7h77BNp/9GRZcS7YYR0eIPf09VWBXfP55ejAhUEA", + "BYTH+IU57MiFLi/M/8zoklxQjBBkc2hASWDK4kU4WmuHs2obIZXfICZnl45hToAemqcVT9qjp/lTtF5m", + "PKaGgkdLKjFDmDhnl14aLGVaWZzculHFVlmiCaciU8mmfEdPj/fzAFUwsuaiaaLqzs6bGt+4rOemzob2", + "cNtsfXqQzyaf5WxNadoS+ORQIuTmy7gGMQ9iUlp4pjeWWNHpOYb/h0phEJc4C1ppSuINvmtR6NtIG2Qc", + "JJH4S4Et+O5mkYquUr1Bgm7JLq7BJT9JCBxvoqVoVBpWVCkXPbxlCu5jrvh5mjfQrNvHTE/pBeVNKaOH", + "RL3TaCloh9d+N25rzuA5LJS/UKX/JGZtZLJze7+IWbfD1rbrvuu23UoVhPCzcWjz8U2nGuSmjDdLbGAv", + "BqZgGYZhz4YH9YY9+tXopw35rstsRfi0hNKB51stN03Pt1vMJjbix2vpxaf1hbaBX0cyBHXwji5IkhFN", + "MbGwkUhVJCSthAQ9rMiP3QzCzhDcwdeI5hrx1edutWSCh8eG/Yk/CKXNL0NIRZolxowzgjhh6Fp3tWqg", + "j7wT4xUYXyR0JMW6lOsqMYBCBdP2uoa65mmsgZ/w9vaOJ/BfdXQxSHrB6HrKhW7CcPP7pVPc3STXkeKe", + "v6IUwRsOBpgClkpx4TLADY66f2JSZc9noIYywUPWlMe+Ykf5FeYbqd1B7SJbSSaciUb9z93dagUR7jIS", + "S5O3bq1jQmZDtjgO2rjHqRUlXNkQXOrsruI+DsWvZjJqRJ16aEz9RnfAw2D0Njg8HTUAwhGCBBzXKdv8", + "rdHiTsQqTZhRvM+M9hd4NFT532tLcwqUa7lBQ6U2DzAOCYljKkHImMoX8A8qxQjfk6yeqfKM716pAk2g", + "WFH+DtbwZlaRx0X0GsfnK7spfPEiUjP8ZSWUTjaVH8v/bo7DqosqVwGhtMvQzbZSiQPuz7uv54ukthRW", + "zVNCNjvvJiYbG6KhOEnVUmg1BJHEVGn7itt8AeRiMUWJPE2j8h3wbDWzV5C/6tsImuA1xe6WKiQYEhlz", + "whLzz+AsjQvUQOomr27df16ssbX1ve8OQd94c0YReOui57bg6jYxRdawzytxdy2lttlCwATWbjzEexfH", + "GmBHtTjYDnuvRCR2GO8Jsevo/eZviV8Mx2V2iIRFmJ1iAIVN9VGXTXbFGb+Uapbt9M02luiwm8sSuhMr", + "q2y3y83uLwoDazVu+iwi/D+MQbu95cIv0mmbnDv2si96uHWKOVo2K0KwTYkyqv50LouE+5q31o4AAyMF", + "R9B3n8ADcMAaAImkUAqr21jfLByPxw/HFfVGZBZvt1i1FpokU2oNOa/1tbiTLIewTiEp1grWSyoxexrT", + "xlwUNFM2j1pI3Ob4gDj0LdiE9toI8C8ifWvh84OXEJemuDLzviTFFdsztHc12ytT8WW3V+It2y4WZGLT", + "c8Yrr9nWe6so5XnMkB0Zl0pF5X/6+ZLG9rXWd+hY4ayDbWiQ2PE1A4VzlqbWDqy5VPa1AnPjr3wZu4su", + "/LsUWRpymiWhwgivqWILPnJlecwYOKcbV0mI8bkY9K4oTqHjbdZRTjFbbExNI5dYFcSr0jtC+AEUc0em", + "c7JiSSCD9dMZ2J+AcHw8hIUBI+BXVIHg3nMzFxJWhGcksUPCvpoVNbxXLVlaPov9zr60i+AxmuMislk4", + "featpHRkoAoqm42ixHDUOaMS+iuygRktnPXXVDrOBQ847PT7HDqEq4Ciek0VNNqZuYlobcMLWpD76tCp", + "DVvyCDyDDIgtxSFfgK/RVMeXm8aTTjWh3LWV9tEI/PZQLUsMnaUZzvgXppenIkmyNFTQpVRsd+dMZ27s", + "lpXt/j70+2s83YcCM5srSPAOPvkGN49dBMG8V23moARtvv6GRLYfMF0kL25sE1b6Nmh/CLFY8yE4c2cI", + "4/F4sIdZmW8oX37H+Rvhe2kbt3Fhh2VBN0eUe1q8r6P2XE+JEY1+kFfFVUQ4p7EnfeeJxJhcLjj1vzf7", + "Gkv2SSdfirOI9/CP5HS9H1k6LA2QZJGkukOx9z6XItHUGtK1gxYbbLy5s4IL7H91qOiid7h0gdWbsmV3", + "9rmsgtM1XMU0rIyUs65y8VOtAFv2cDLd9FPGcQuVt+Omi3Cb9fPVlw7tuLxA47XY8gn7SOIrElmNOyoJ", + "E4MdSfJp3nv+1w7o3vttGKgh7ubZ+bWXX9vVws2ft3f782/D3g+UJHrZEgQ4m7qsl8odl+s9bFkhS5xz", + "U87pDikOF1SqcGxhwD+N1kdlM8UEoXswUqbwW/tCCAEkKf1SJdg/U64IRESTRCzAj3sBGfdI+w+f+IDl", + "tdzAUoOLLAlVc6x4mLtk81jHRMfR6LM4xLmUA6JYseyntvPuhvRbSVZ0LeR5o2cvVGF0rWBFjK0KWtgg", + "obmfx+V1Vv093smzff78uyaLugTOy+7CTDVu9kXmzstazVz0hTX6t5p2MO5Wu6HyUFJd2J38qNgcEAUE", + "UiojyjVZmB1kPLarG70hphFbkeQFHFs8L33JFByP4bNYU6nyTOCEcqOMCEl9Z5CfGF2PiIJoyVJVOcI8", + "EWhr1TyENaysXGcFrmE0LY6/B6q2FEnL1++utzTRQ0iHuaCSJMnBMzYBC4Wrm3s3HN7TtjJxVeXgAAjk", + "vDdUbTRLDp73NEto0FaLCMeoTvpVd5nxLCL8xA3vbuptg7DR7ivvZ1gyAyu6j4VEt8tq1EPzILUOjOI6", + "hVAHJoTyZF/+Yz9C1tPttcF7QK9ZdgY5Uc356qffj0chku+htJx0UVecdT0OB4tyLUUyZXHwZQZ/BBar", + "PGchj/8pRNUL52/KOFak+7f8h5fm7hbsguLa3YsS1zJRaoYV5oAraoyGiBq5WlbHSmP9tnapbOCBaCgW", + "64YPAV9dQWkhaQzRkkbnIDKdZnrcWI0fR13TU0AVAmd2W37A1jkNBwJfRRo9AsFdNzlsalqPA7DZqpXy", + "TNk4bXwrHKJuNgSH/kPbc24QXDDvJBLGYfz5BcxJkiiYkejcsAUHoQP07sZXYN9MpJpMVNKHS41HioeQ", + "Ep1sX/huwj5zORUhszV3JNxwaJF7+6m5DBrSecoFX+YJWeS94NzBjDbbni60YjwLBnWduNIpRqVULpDL", + "fpNLgowrSm3YVqg82lc9RbFLgvq397j4hBtXYLmyddhQfXDAXp4wM6XcDGzoYYB0aZM0wA1EUIZLheZz", + "piRT7bWlq7lHMVOpezvyPibMnbAVUvsxU3ZpIcGmvZTvNlTGYEvBqZ82sNvhNl4HUCGAgR1I6c6rQ3dP", + "C9kN1asI9SN5sF/XWL+ridIrXc8O5TQcn9I9wK8c3Lef2+SwqD68o0MTmxtKVrjOPdOmOgzb2UL8gknB", + "fQp6uYp5aH504B6aFF9+Yto3RzudkjiWLnKwtstd9TaE1JUUkmdPnz5+urMgl2uuliP3LtDUFd22chi7", + "n53cy1fp3E0o9BpTj9pysgpefYg9nNvCrlZCh4/LFTKSUsRnN++5mSGPE/3t52GjzOcC8rqotjyaEf6o", + "3cuM29Y/Shda7XgLSUOQr4k3d/om6L8lLKHx5U27kprfatIZvbQtgv1O2ny/Txvqjpk0YSOm3XapIsxu", + "HG7rf7u3e69EGgGcsAGZje8IeeDoq/fvfbiqdap7xHV2qlB6lCPoaM4STeUQUklHmCU+OCR+tLq3XQ69", + "96yTBpu/Ube/nQQeJ16aL7CsgJ8jx23oR0TREeOKYom1Czro+Lhwr1C3uPW27qrt7t9pugrEEchoyTSN", + "tIuM36kdWUpNJROeq3UumjlsEPL7yNugyG8RvnWDO/gA1ne+NhsGMQgI4DtRnKquNe/Skq9PLT64blZF", + "Pw47MUtelMNcIXdblasW/VhVk/lLzpd6rZLD4SFUKZSzy+hSlMTO4WlCtNnW1PY7mjMqu33nrJ0OFs1u", + "E+b6y4aFbR13jCrhtfHgZr0lj2LrrLfkPH2XZd+c5lfB+1Axma3C1uErQ7qtdjnYTaX1mrwHEruZJi+8", + "byG3O4jNMppSBsel9lBElm67z7v1mqgHoLV0RDCrdL+ZreGuIG7jeMkuWEIXdJ81gt/sWKihAcXl2kUo", + "tdxj3/XRrTsOp03u7ONgKKxLbHWj096gHLwEdGm7uv0vbOEteIn1MlZ0t7vcz964wxyBf2BKN3fvsbof", + "kkX3juTIFkolX3aS056MZI722zQhGyt68sQkpz2rZa+EowYGV1oThXW1CW6cS4jzFvhV6oTTCyYydeXb", + "2q0E5Jyj615D7Vt2s3tD6d1WaM9OK+F/4D47U1dbwTO9d9hTA/EG9KQrSKL3G2w6a0sVsZqhWWVzmHD3", + "9dtn02dPhkDMWKSia7ZG7026azfpbtXYqSV7MqWlgHevYS7FCo6ojo6EGkmaUKKoS/qUS5oMIZtlXGdD", + "kCI63wwholwLNQSSrEjCePZ1CDGdMcKHIFLKVaboKKEkHYJKqBq8ABv/7pMq+2ZS+NV9A7+C+QB+BZKk", + "jOM/ZLSEX2FhlhHwKwi9pHIAnz6+/5/W7nz3GtbG0Fyl2hf9SSUd5XUUx3CW0sg1C8BgiVFRE/Hi4fjx", + "+BhenYwePRp3hOEVmIABAvcjn9Pkuy4b+dczEsuRodvPrJm01f0a26X+hSTJKEpEdA5+sA+eSoimSttE", + "IKppjA6L/pxxppa2GOoIsO0J/segkiZU9o5h/zm2okqTVaqAzBTletwpdaju3qnuvbyVpj3bJ5GMN+5u", + "fHAAjS1gWEb8ejM8bJnufm+DauUhZ2t3gcgdwoOdLlh8RXA4qB1G5bKKXW4BqhGTkRe943MRyiBWWYK3", + "TCDnYeY8Y/B9b7zfdmp52pTxuQBttj/hkUiyFR/NhRzZf7axv/GEbzWUJWlK5EpUYqV26577u8pFkmDi", + "0F5MJ2bqfDqXlE4Xs27qLX5hH4P2+iRTNO78xZxJuiZJMlVUXrBQnJ4fEcOvkM3X8CvwOd6Ygl+Bpfk/", + "kTq6kGSxZO4f2P3N3+NuUuvQpOOdE59TyWky3Xe8U0P2+WQfIb2iqym5IAxHTVcdL918ZRGr6xeX0L+C", + "qtd4PA7rU0dWmzoyutSR1aSODIUeWS3qyOlQ2C9pS4e6cn2JxV2d6yyeJuycdh3eGY2EmqaSar3Z65N9", + "UKgYPp1nNoXo2p4HFEU9u7E17RvsPs74An6Fz67rwoVRpV/70FLHZ4DNgQtt1GVlSxbvXntN0r2wvtFa", + "rgiAJkG51Yr9bsXW7Yhau5T9+YcPl9u68Hdc0yRhC8oj2tQEZM/mFtXIaxJfGFtbAUaks9JyRq+ebYDM", + "tRnmG0bPswROM36C9Zf7ReP+rRatD5etTUsfX38HC7/NHY1r92iUWsxY7TnBuNK2Za3rklpq3Xu8V8fU", + "PbpEdOwFsY1Bl20HEcDJPTpChL4+qClEeaI3F44Z7V23/8AK97ZFx56aum2J4Ovx1wLDEqFoDJp8FVys", + "Ns8BHSBW4RinJDonCzp2PoneHa4UVw5f9M8CxhbrDXsJZtusaMwy7FDGFsvy+8C+pd9K0KyEDpa3Xb2o", + "TlikPrt3p8tUH9zGzYBoQI4bZVKJLjpO53KF5bXPdFPZkQMMzX1QzCdV7CKspupsdq2yNMI3k7GfF1s0", + "rfKet3gcIcdnftldDRwKNFLFJzvVr/csolxZqLZwUWxcRmVjJ5Mr6MIwp0T7l+ruPk3GpwtJIjpNqWSi", + "c7EU18vOKG8E3+x8zf5hj4tpYoGCuVLYJif4wqhZ9bV1LilWlkspX2P1uDTBpEVqJF4qmaLBaTIsgJpK", + "elFrJdr0IofrlpIic8C13O9PVLL5plHDdgee/rLWux295cEdlmx86LsxnGmYpsAhNbW3HEQe3dVgu8AD", + "T20Hj+BG1gT7yl1qu3Wm6fdeXz90Mx+FsT8j1B5OloRzGtBFX0FME4ZugMiOGcOXJQVN5IJqUDSSVEP/", + "x9P38ACwbCg2hjr78OUz8rijUjPVATAFH9/89ObUddWj8Qs30XTJbEETbMEn+MhNbIPFzez4r7mQoBIS", + "nR+t6WwpxPmwWAl/pCvCEoxX3eocu/djR8kXGsig7SYmGh8TNVlMbdB7swT5547n0x724AFjwrlG2faG", + "fJlS73d/CbaTFzHDe6E8uOIOArm6jbdRvudgRVEfspJX2zRX1xv23OUZbmjuK8wIr6QaqSs+6h6hitzY", + "8okrl7FfFdIACRU1SWuiv466oLI0TTaQyQT6S61TNYQ0myUssgAdw1uP0H6oWukUS+Ud4b+MxX80l2J1", + "pAX0CY/BG+NHvrswMFcpgCZkAyTTS0OLEdFUDcbwKkkcBSsgkvraCpRHcpNqGmPugn1G8QS7TVqVSC3X", + "GKvpxcBsNuS7sh2/4K0UK3AOEOgXtNxIVpfo4F1LPTJ4bMADOeT8+nBWg0r40cpfTcPc9gIsAe0/sfft", + "NE5sBviJDaIb2+rpt/9j3Htu5EeD4+cQBrTNO4LPWRFLGeXaXyatXGd3fQ4FSkuzmxklkkord5D/e9oy", + "V9kRwJdiUjIgMr9Y0WjYZIiut4VYx42WPW1N2OvH7IVkdY2yxDM7Mr1wWyknjrpblSGNZGezdr9Ix60W", + "lSqrMLR/V4Ds0/G0o4JIxnCGPJjxhXXKWYnoKkf0DRMPKCfmL54xlPQTkDRNSEQVMP0CxIppbeZlGhKK", + "nWZxBZ8NiQtl3JXRb2XAzRz3OlhoOxNs52TXzZAO4DFN1H2JKJia3hFC0U8qIslrEWX+uaG7N+EVh09n", + "J6/ew8Px8fgZvDJ8Vq3Qj2v7BkLs5oX+n84+fRwMgWCaTpxFthcs1tf8RgH9au7FYPmnlPw9o6AFfEop", + "/4tRJNEVYe6dxqCXUmSLJVxQOSOarcYhdfJz3vW7KWy6lGC9reIaNJciU2GEPrAjujdTF0R3abJqX6uK", + "zN1qXaJiiz+3Hl+d0gVTui3A9YDSfr6YX2NYa1Nz97ZJ63cWKhQo9skkPhUJbZgqXPXU1uAr790vGQSy", + "SFjEqDqliSBxM3xFprGn+WV4Sr2iu5+ycV+b1zRiCnv4NTf2P8gtv7tfrNtduO6qjVRqKK8cCvnq3gcx", + "DKReddGtJVpbIZ5SLy22XnH03szyC1pBPKZGqBpuCKlQOpM0Z5LW4CIc6FeXAQMuJ/f5hFdLiw+haFc1", + "BN+2CNPNh1Duq6aGE54nJTOlMjNAi3RaGWRDsrbOf4jTIiaaTImainn3b/yoUDQ15VSW4ruvqo1LDuJw", + "2lIkUjpNyIyGBXBe4aGLB8CVYfANSUpTV6BVO2sOlWGObpULacbX9rRGiWP2YKOWBnbxUD9tcFs5J94V", + "S9FVmDI1nWUs0VPGw0K6SQLtUMZC91eVvFXxUN5H8ORZQsOmyX5VMfw84ezSUnWDWqoalkq1bdyYzbxP", + "2EzayrO78ulwg23FqyqbClYD4waDE6w5agupY71IX4fDbQVmUqwVlQGfaauWtgNziqLRks5bpe+BJUgr", + "ZalhNMmOjx9TiEqVcvrKGMJqSVJq9F5XPabeULsAaAOyS7qiMSP+pLvw5LQ0vLXaTV4T5FdYssUSfgX7", + "cgy/QlIu21jxGe8JsSZGOezpot1bEHVdwZtvFBB0PaZEL4EpIBCRVGfSGDRAtFixCEpzQd98OenZXyY9", + "UGzBSdKhgmFLGdBy2ZxCJUdobCFa/WTV+2uio9PqHW83yxkxHtPUqA9cQ2lGryHYCE5z9nNLaZTEsBIx", + "TaA/J5FWGHf5AiRT55DQC4ollA0ZEC0kWK1oCGLNrWGWG1+Bt4w8arUhORaDO13fshX2ne9zwUe2i9Sg", + "snv6lTVFDa1otCScqVUg2DBmSjMeVSFRfOBb4rBVmlCjWdnOjS6hyLpMporqIbjQaR92PtjLOSjpTAg9", + "ndEluWChZnb+Kswwh4jPYWIOqUcpkWQ16UFfabIwMDdjrItlCOaisW4RfjoAIWHS44JT8wEX2lCBSz0A", + "h8Nq5NYhXK2pHIQb4WM+ifIR4wHI+l+c076ArkQ7B7tr+mn2AFaN1goMqtxzaIfbYA7REGbpLGl0/uaC", + "YRBeWCBFYrUiPP5GgaTWDcEMG6HuI+j7gAd7c5UZQ4QQ6YyENUS3UkN4AiuClAIFlr6m9U4r5ahxvRQN", + "zi8dUymbfhKZbuDDtjh43OGp3y1ePkHjZTSEUuZFiKcrFur7ekpHKCd9XJ65EiwLO8j1hnwK1zXM9pr4", + "7hgfi8hMXNDBGE4SskppbOYR8NenQ3j07bfHP/dam10dtCMMe/TaVS5SyzvLjM0Hj44BjbtNMWjOkMr3", + "221jXYIPRGkqQa2ZjpY5sEhMUmtJ+ljKMWB4p61YgHpYNahzu1LwGD7xUUwNPqMSY9/qMk7mc9+FJ9Tf", + "co840/Yw00Dt4n6peDFWYhiEN1Grgn040tVnqtzw/zgGLeDb7/a7yUrx7sN3Vpmmsq1HuK0ne27L1RA/", + "fENugspWnuJWnu25lV2Bw5b2cvSIMWj42bEqoZLBovqaD4fw8LhhSVcs4bDT40t10fM09q2xOx+5oUpI", + "bVt1lhVEpdpFNtHCsLf1h0NioAuWf9nY55Lw2CPmufzVQbHOZoLXuUu07jJRfg+drHUzl1XqMfSpoT1N", + "l1na2slYban1QGHXQy0qdrv3uzFW/80OeGnIdk6NREFeQ79qSMmCvnBpxlbXp1+XJFNWEnQMYjZiZC+A", + "lqoJt6qWduYmiJhraVcRndE5z5IEYsmSZBSLNYeUbIz+i09RpcfRXHPM+Frapl1pkilYUU1ioknAqWF0", + "yv2OXlVsg34Jj7jV86CzfmRMQbSWL6iMWYRvxq4PUbjST6Bhj83VKuXuFT338xKYN+GB2brUw/umn7N0", + "2viY0NwIJTUE8iv62OFX3+EEfm0CQ2N13jxetuRccPc49EiyBcM2tD7NAz0bkTp2bhOPCNguGlW6MXwU", + "wLC40FZnm661qetdEqFfLXE86VXaEE16gw71WapLCE5Hdo/bnX1AzGG9JLpoTGOhWCuR7b7Mwkket+81", + "DDkGl0RNaSPX0qWWH0zZoy+JCjUqMteATA1VkXCy1kGVs9t9ifArTHqTXtG1Jch7bogiG+tzI7e04MMR", + "hUvNY1s/WLN7cMma3AFGsOVTrGBAMxfg/5HRjAY6dvwd/75fnlFTFQz8QWZcjVkML18Czg2/iBnY/y75", + "P9W4qFKxO6NlKy3C7np33lKxSA7M4sBN0PLNlz5LesHoehtmsyw6pzqAcI+egKu6NcQOdcbqWIpMWoTh", + "Yv0C4swQtatEbs2U9VIoq0FNvXkSYzkPVwmecWWsWW1YmJmtpT9KRtsq+ZuPp2I+N2QW8CVnUuUbhf4x", + "vPROFWNXm28HvQ4J1cUSw9J+dvc4saO5WIfd3G1gMjyNJEab2fhooL63/ZAmE7EYHNAL6kwITpUOrukc", + "ydZ9XfS6whut9u2uNI5S1rF9cOkbh72/iJlqoT+7kkudTTbgyCXci5Zzxhf7zug+240Q/lKrW6+tO8xJ", + "qpEmixLk20pMZZeoiFvd5ZtC3UYXoIv8sPSntjWYUlmjw1N99knV6xDVcgB/xgfaaa6wh/sPK4zzaPvd", + "UFLb76Uy+6E2SrmM6MLepb4k1JtUAAs++NUjLPxaKs1kVYOwvqclWyyonCqRyZBuJfjUOT5/zQl8d3XG", + "Qh6VsiC9ZKotWb77UHRScUmVG61fTxUfmujrJyKZAfCnCyoli2lAuIjyTwemBfllMJYZ+7i6SeGCJBkd", + "w+cfvxQhyEb8oLm9IunOtNJie7vO2NKS5MIPCbUPkWmmRpmRLvkwUMKgLsw24KvJhSWz9YiraR7d0dCb", + "JMJVXGSGpHMqKY98ZLdfNvxYgd6sTNJpKA7/k1wQzv6BL3YjldKIzVkECOelSGIqwTnBcKH89VctRZbE", + "QJI12Zj9oD40DMa+uvya8EsnxsKMGM9XYRwoggS7k4hMA+E5MuyVruY+imlDwI9TsVW4yWIZxKjgs1gN", + "3a3u9eSL+BtIIMJDsguH4NDPEd46S6geAvXvLQ44u80Gh2se6H71CjCGNZwrQaKGLHv3nyuoJPTpj4rK", + "HU3obNpKe4j/4x0h/i2ffrsjRWWvxILa2fN5fPJNaVdN0GjxfB+S/LmqCu69Ix5tOX+36Q5F9bcVDBFk", + "YqciofDutQKiFFtw26jb4LWB2Rg+izRLiOOWNsxLaaA8TgXjGvr//uYLHJmhavDCllUFX0JawYpsMBsF", + "mN4vT+sGaoluoUQjHoiEvkLYNFKGAa3Tm/bBSv9Z08qqPfQTod7Z81xB6V28wk4d2tdPVpVp3pURGnFe", + "wr6Ka+/OPo2+fXb8ECVLXFQ5DTctXwUfCl/NZkaiIUouGNbstE9b27HGgUzCfxeghUiiJWE8LzVq0HrG", + "OJEbLGCHYg8lXDDy2IjGgMhYzWgc55GRlC8Yp7ASaEL6hfr23IzPRdBZmhd3CKUo+uwZRVcXVEI/iecJ", + "WagR47/gs/1uAVRM74+BQMphPSxf3vbl/4YFMULlRs+w0f8xrElyzvhipM5pQjVGs8k5iah7ZpGU5pxD", + "Wc8I/UplxKwknfC5yHjs4uA0ic6hXypINAQW01UqNOXRZggki5l2rciBuozfgYt8tx7BEtD6bosDW8nA", + "Gm694/HD8fGIJOmSjB/6CyAp6z3vPR4fjx+jmNBLxOsjkrKji4dHWNvDOU0XIZfMKWY2Wz0Yu12jYQCn", + "3786cW3paAwZdy5uSSPKNWBtHjWe8BOSJFR+gwWzfGQyxDSyygcz94/zKetkZrNM0xewRO3Bem0m3AVv", + "w1KsYUX4xroBrOfTzW52g1ngmE0bg7I8/8d3E27DWjHIZdL7CBdMYVDVEXxwy0x6rrojSdnIg8MC3qqg", + "TPB3sSE2ql95aOH7NVlRjTzrr//sMWfoodPU8u1ebkNZplUpqeJrDRTeSNds2he8MbqTwYlK54Cgs7Fh", + "8VJH+K3lD6/IFF6sZEjma+1sGdCwb8ZrMOtW3CA8W8Y1qkdXM5t7li5P1/FLH7BQfJgbK0+P9yoQ93PR", + "1AIJ+dHxcT3pJ00Tl9R79It7rijWbROqHr2xBBYyyJqwcr/jI7thME/s4qE5800efU9ir2rgJ4+vbL9v", + "DLf05RGCG+aCb1YiU45VoAzJOwn2fuQ2ECY/15zSGPo/fnz36SMagrYmiIIHlWcDeABlSoUHlnvDAygo", + "dYBL5Vw2XjF+5JIsn9vaM6hruGTgKqP5LJR+Zb6oFAfqWelHlf5exJsrg2Gw5tFvVVnrGtFdG96FiyAF", + "7hNHuDVcsgL0XWVKLDS/SWkMro3ToHbbr+VmJDMOWAKIaAoE/vSXL+BuJfcBYDHHJLEZ54FbTF1q5XMb", + "c9zhGqvJmL1rBGRD2mcAkp+pHBlouchpyLM2b5pErYbgGglbB6OHbPX67JnAWXV2JMxZQlXpKdS7FGKI", + "mcSacIZuvo48Lo8KPaT3vLe9XH7VSPc7lSLHHlx8rfsvLFyOOuJaMq0pd7bmhLv60ThuJEWmqTSKkWJK", + "IyMhK8pjm5B+8dAoc4MxnKDMmfCULBh3HRo4lIocwus3ZydjVIGe2y08l5TEVqmZcNRqcGNNOo09ajeN", + "BusJBhUaXyOOROdcrBMaL9B5pVhijuawXiQX+M+YKXMLDa+n169j3KRudK/Q3KJCg7jdqM5Yev29KDMV", + "TlkQesm0qnHM90zpnL/Enj31HSeh8RCsAWf41SDA/o7+yeLfOhmGOB4fPvuMR0mG6UHa7IuTBKPxqEKO", + "6HkAHE14zgRccS2WJIBYhvtp4mjQjaF9v3n3uoGnGRu4QGQW9+qqzj4c5tqxtxFx7yT+mS09uUH9HvGO", + "Cw3oa6nh/xnji8Qj52wDLG5C8uclqVVW6KqrWRmXh8WVJV0VW43cp9vouoWsqCGa316Vlr8ppL16UwKP", + "8p7NabSJElqyJX67DSKxFasc27sLxIJYcaeoxaz/3c2t/87WB7a2NL7cY4yvr65vlcsqCZcIA3N2LQW6", + "K22gZSdXmun4Fd9gzFQhgGxvf0/Y+d8vSdWv3UbuKfqeou8p2nthLFEgNePu+15PbFRBnzutcZdk/rUs", + "kX/1lmdO1l73vCRVn7rN3FP1PVXfU3XunEOiyKm6kZQdVe5FyjkFe5IeQ1HiVsSbMTpFwEWUUTXhLt8k", + "/wJoQlJF1QvAO+ULWFHCFTAe0znjyAE+E6UBZ5pw6WzbJ8fHnbhFwBDN+cWZO/E9v7g2fvG789vcs5j9", + "WYyjo0JxsFRPirgb6AtZIulkU9MospjpIxuaUPJqbfuPzDjbC6ibVzx/v9/bi1prUHXADCTSQvrqkXt/", + "fe+bvkXfdIFmTQ5q83fseozPyw5zD+F4227h8pTQt7AelTzDnK6p0jBnUuk6GenlyL6ctVORXtpqr71r", + "BWK+SojzOnbidlsw/nbIvRVyZuOXq4D7idE1ahVrIc9VSgwzKlpGGB6W+gM3vTjaF8Dn5rOpy6aQlCD/", + "TbPQG3JWB+Q1iPd8gWpr0RsOAmi/StdO3XfAu5TYP+z2T21GyNUjACoDWyR2lIiFrR/aEl2Q6eV7HHZ9", + "mIHz3yJKuPWbIxlwgG1PSmNsmSTOqdHtlco8pjy8ec2m1NgJhIQPb19BDjhELBplNlv9rz9XYpK2etQg", + "ImB4Cghv9nz59OVzEGVcIbWdOGPGbd3ck0AsLEXMBUkvxDndjskwf81jMY1hpWjxFFnZnI2gbhMXH+h1", + "i4oPtA2T3uGF6c2N48xHYYOSPPAMwtjeLVvwNnZpBd4s3/Q2wI9qhYbbgf+5Wlv+eu+h0gWgNUzJDcNM", + "jRo4isSmcnhxXolvN4Tm5DnlUtjm5O008+Htqzd26HXDxi/UChcpLrCEvjngj6fv8gZURWm9QjAJCSRN", + "67DDNSqAyhT2MkbmYhhWGGCdohftOa41cLGyxl4CKsDmDHvGg7FbEBlmcQNy1z/TFjFNE7LZ4rdYREyu", + "sAo/3h+NUbK4LjizjT0EtuEhkFdsh6C08ALmuc2+2n2jn90HJ3b8NWqjlYUue7d+toDyeEPsna6LvmmS", + "2vwWc1llu+QG8S03i/ye1lK4/JoSqiHst/jDNyr/LIBRBRd+Ll1vl5024nY7mGuNkG3pPhMAVb6lKnDe", + "Zklik078MaFf9GkZlsXRsMg7xpTFbWP6SNK5pGq5mwBP3cDrozy3wl3W9w012Y52KWHyttR8Byi3E8e5", + "h+A6Iw8tD8c8/X5EVERi6lRocBUWaDxotQNOBcYk2ELXpbVewIpxDQQ4XQPByPcHfoABSCN6jSIhzlnL", + "y8spJbEN5/tB6/QTTzbwt6I/s5vlb2CnGYIUNqwPk2t5TKXt/lbZ7BA3q9xunXY7xDooZ1SPTnAqBTOh", + "bQVHO2ecT2LXsqWTwf6ptCU339/G8KMqMnpdwwZ49fkdRIlts2h3ic7mlEhbK3D05PihUZvkBmy/UYEM", + "TllLi0Bi9Eq/kTXjsVhDLPg3GhZG1IoMH5e1AGupg+CujyzjPosMXdMi043vQwXFWVDcrv3jr8Pdwotc", + "c861y1sjM4cI7vVu2EZwY/jeoBP2e4zciYikECXGoorH+1JdCQdLlOcttb5Ht3ki1lXeXvREOqJfI4pH", + "ak4peJtQqiEfiObOMDf4kw3kyZazja9v2XdigMbDCSdpKsWFAYVXMYbeas/BNBjDx5JjdwiRrQpK9IQ/", + "Ld46813U426LI42KIzWE4Z7kY98Up++cZWBrzm0F5+cH7g17/sBINL/4Wt/eUVF0qe8eqt/ycIBvBTf6", + "PpBD7T1TwYfQAqxVd/3tPnBWcac5OP1tUSK9wKoS/mPdoSo9FR61NqXypDTsGq+nWKZS1iAEnnxkUQnM", + "OjNu854KaD63LxDbb0RlF2Y/37rgyWbQ4uHemnjYotXWL+vqtdpihWo9mE6q7cNr2EZHVHFVWW7cWP3J", + "ZkfaCgXOIYFd4WxTvyJmAEWuH3F29gOc082NxxF8yBLN0oSCfVeBvIp6zZhFYAIpoXQ3DN5+mykRRZ4f", + "E9OE2qbJVQx/jX8vLvX2klICbhG7uRj6TE1t0NVL7LoxuPFglBLWN6ZyiLkeWTAfcIvufrBh4y6h8UdI", + "HNqb2Xjmfhev/i3W9y9feqmk5j5yqIWIn0eJ4C2m8YlIme87buufVHaTd4/3jtgV0VSaQ/lG0La5hjUd", + "pFhbGxNtUtsNHznske2+yeKaQZsl58BWqZAarQxjcxKtiYXKUijKS9sBTVdpgkaMAGoGcbpONm4C1/Uj", + "T3aXdJRKsUqxwbp/Oqgdosl6LUlWhN7vN8CxdpK7rSKYHd6advBup0ZwkesPvg7DjTOVszqJFlvELleu", + "SrMqZMrNB0AyNXW6ykuskB+JJGEx9d4n13sQH9Qrmk1dsUFsKDPHMqfBW4K+m+G//vd/2gtzfcHyPwwO", + "1YViRhZcKM0i9ZxGS9HuwX5djH5jBof5xZKSmMqCY7wrqmSN/kw3reyjUlPx6c6aig0r/t+jkyIsc/Su", + "PSzzmjiSAdAtOeHt0s0MyPxehNscHIn1dPcnH7Ch30ehXyWJWN8CkdZwz3v0kURjNseqrRpTEOqP6wZG", + "1omIRwbXkv5FXiFOlcu/+QpCzcTF0R0y0lTp0S9i1p3Q7IdfqNJ/ErNtf8ijqwNmZaU2BPqTmOHDRYo+", + "5rWQ51TCmmFHI8LqfiKsdDc6dn0cYroSL8CBA98VxEik2N4hlSKy1d+c3sT4yP3NLdIM3guSZERTm87R", + "Hbjus1eumNu1MILyGrfEEWzs5WsasUqtzOYozdgNbbjK1I/Cu3QHLBWSmOqlpGopkljlT+QNN5dKumLZ", + "arSX9PlsP7oTQuhfU354Xv7o5ni5qzEGsbCNOsEWKqHgUGhaQiuYU6JRc605RXGK0QLfKA3Kvcj7G7dO", + "U1Gv/B+xb1n9k0ZEd5+PjL6GPdM6Y/up/fIHofQpxfJj9xh/GxgPfVuDECxjMxeJjonB7b4D5PtoYNYY", + "5WJZdY7r+Tfl551mG6K6xE7stnbGAej9F/zwHr/vDn7jVd4FBC9s1z0wfCtLcweKtxvIHsdLbetH9CuN", + "sv2R/bSY4o2b4R7r/9X1mBJeTS1e5YnFt0V6pS0996jeTIMPfCXWGi0GZoEH4ePu0rjCMGom66YDePou", + "gmCO/vkV64jZmJTml4Q8gKUoI+bCWMZge8FcMLqmElaZ0s7JkNc3nXD/uYS+oobifW/RONMYefrk+LvB", + "dhiPW2M84fuH8hgWlIeavHLn6+Lr/3o3nf35WU5tJ6FrrmOQLxeiFnPj/v6L67orATxuZ7dZpyCH3i3W", + "KviITLbszvMVw1zUoaNYpipj6rlbjnSAlGYpXXkbV7HhbfswFR8Qty9TaWcfpZLCh7GPU3uQe+5xZdwj", + "Tye55x5/aO5hKecw5nEhztsKLnn5U/AOjKANBQNjAkM/JnxBpcjU4Co4Au7uniNcIUfA67t7DMGhzz0/", + "yPsFeMoLV1RDaBVFT4vQ6BmdC0mBaWXj+qvPI/OEUl1OPrDltxsTDz5xbAqNfcKLmP0zzINgHBISx1SC", + "kOZ/+77+/XDCueBTv4oeQkqkZiQZwkoonWzKP5X+mfFzLtZ8MOF5BFTeiBwTbsrdyLHif3GQvPv1hJv9", + "Mtd3gbhyUWM4swlJOPM/qBRusqK6eIKF0CfctYc6o1ozvlCgNE1TKhWUmkURM+ssoaDY15FZLyEbm8qT", + "m1CunYOKCB/ZzhMNqRAY616D7bUGp4cXbGgDYovfYkSIbUp9RxzSO3IHyjtG5C2lERQE1UgV2txzI1EY", + "uMRkA/gZkMVC0gUiF3YFcslkDLNiXWpY33eVfXw84THZqCFECVmlNIaH4/F3x4PnQC6oJAsKKjLk65q3", + "K05StRTah+ap4YQXJxuCFpokGErl28Mqm0BnkTsiUmLenafMCZ8zHhu0HsNnsTZYbbaLo0epWd5toySw", + "IaaJJg3eAQRUN8T+gjDdkuU1GVUDHONgwPUiBxcGMP314RC+O/4Zm6JtJ+qYD8J5Oo9vOk0nCIIAjr8m", + "LPH4ZFsGDkEk8R3J2ulAdOUDlFBHuwNvkxrn1OVBHs0kJeexWPNGgntNJTOiMA0xpDwyNefY//W//xPO", + "ImLbqRu99dGzCS+6b2FPjzF8T3isANvto7X7t8igQJQZeTp10YnqbxO+NIxcUsXUcxA8YZy+lJRES8P/", + "H9hvXh4PIaYLSWIab/1oFeeXD4cT7snwZcaDo6LHQzCAeFn58vEQOL2gcppKMaPxSy6MSB5P+Ik9v8pW", + "GPVrNYECMqXUPIT6qAz1UQ71duotvvg+v6brDE4PLtgqm+6CWHKFu9pp5Em+4eKMkF8D9C1yHXlEOvLY", + "cmR+PiqjwCBAUkZB4lS1Zr/hnb73A6+b7+ULBR8q7G8gRZJk6Y2nDr+qVmYrim/deSz6ocT5ZhtwfIQl", + "Bplcbuw2ctjOqyNby2U3ipzi8BM3eofIfoupx9hJu9wG0Wrpc/PhWsjzqaRz28eVMOwLxBSc003Ry6xB", + "mOcT7Kr12WlT5oNIJxtMiTVSg3C7ldO3J48fP/6u0qb5+muidq1F+vD4VouRBnAimI2P3X1L8L5c86x7", + "btCBG2wDXUHfiRl7VYN6/vcWc0AT1WZUNylgTtMQczCDpzLjyjZads4K/DpGy0NmVu+yjoXxhH/xnb76", + "qBhSTeMjo17ReAA4kTHB6Vd8p47H4KIYlDV06jUGMAaoUDDbdBejAf4Hnuq6SaNYKXCn/xEATUT478Wa", + "PsuRA2Ka6iWoNGFYTCzxNd8bDWo0ZXcKmzMc1VnIoN/HWLpTi+aIhjcqbH6+fnwSMnh51rVhwXrPI7vy", + "yHAxCetn6acEi8YcoSclpFdrkY6cgwW5z27t6YtI39oPfsDxvyPU/gPoKXXohzQVws99rqs39O/VlOs2", + "WlSR17zykM+9ltA3S1D0Te6iQvxgDyo8xfH3VHg7VGih30yFBtL3VHgzxgJSWpgK7YNBIxUupMhS1and", + "77/jUPua8OfP78Ctjxqw+X2eucc2Y07YeSe8r5imyhceMwCGT2dQVM0cDG25Atw80+i3TTNNY1jR1YzK", + "CbeOJB+XELId7FoNJoPd9XWaCrjCrnJQZw5YaZIpBxx7Zns8dfPlYC2WMRtOXKobfdsY3/klEKGYY+ED", + "V4fR/jVHp8IN2VDjw9cIMngK/RXhGVZGMbinliwdYO2AMtJu/KgJNww/ESR2i5qRmRbuv87phsbYtV9N", + "52TFko2PsXMWcL2rWDMafxbK4vE1pZvi3BYSN13+wh6rIajF1buwAL21qhf2Ol1/qTyA5Z5Um/Jlbjq2", + "5hUvk52v84GVNPwbItGGgC0VNhUJc0i2JRsD9b7qMnIlLlwms91D3/EPcEWABxXpVaJ7G0PXTPi2Wpcn", + "/TtVQeyeCjpQwQ1GuSGSNJbTeu2LqOWsNCU6WobK5utoCQadhqCymcEQG3gSiUTIMfyZcevzLEQkEEkn", + "nK1WGYZONeH6LhlnFr5ZTL8mQWr7ad104lqrIHU9EG5ZkGYOMPeM4/fDOCwye8bxjYKYqTQhG5gzmsSq", + "SV4eOfbQ0o5fKbbgCoh112H1Kqd9OyFaCHUFMQYKGRHL5IR78YpvLzZEHj01qPU/OT4+WN7mivYHXOH3", + "zonsKS7dJQdnARLHt1B67oRwpNo4Nvhhd2Jwpazz3XOU3xNHeYVXGSb6Xezk6J/oxO2okLtVMAiwus4V", + "6OM3xyGGwUkdIK5f33fkLxGo92r/LlqrRXGsbPJnEyI2IfzzFWHmRKS1a/4Z1dbsLI2GeUIWYLsJivn8", + "8pKwtJHfuzgsjnJL1SX2VdLv6ez3IdO+iMUiKWvJdYKs0PmSkkQv2x46f7AjrhET7QqtLxZUXrCIGoyw", + "G8Y6/09vEg9KW/DB04atZZxcEJaQWUJb2/rkscgPQFISM/w3BlpDn3DBNyuR1Tqz7QwEaYj8CD3rUn7B", + "pOArA6cDXoU1WdxatJI55a4XLYxXvv2OJk2VzLCXydLdVocaZW39SvylX4dk+sHWVL+9HiW2ROCOe779", + "viQ+lAD6M2JDfKyTkqVDSIXUQ6A6Gg9u/PXhB7eT+sMD41BmAA2PDuYc+xcYQ7QubKFyY4PnkiqRXOwo", + "LvZDtQnPqfumi353PXbHTXfBcCcuN2y/vXYYH0V5G7lsw7crX7q9tR68O41NrC71hFgSbUsszyhgDWsz", + "406ka2uhUce8upmSBawUVI4YX5SVoulKxNTVwCeZogolyWcjm79gbwwFWoA6Z2mex1q0WlTZTBleyTVo", + "Fp3baje4paTIPsJU8YTONWRciyxa0ngISoCkKlvVdgMpM6IkS/PyOJAQpUHSSMjYp+qP4eLh+PH4OGgu", + "ZUhT+xpLV0ZM1yOXbt9g2iWcfnQ2E6L0TdPtD5U+GDWa/GzQ+gixjUKOl6h9KkeHinFjNVyNDCjSYI+W", + "TGlR6cEcDurKUkNkf8Pww79h1NjI5pNZYipmnLoZbcik2XypxOUY3pBoiRQXkVRn0j2Fp1SOErKhEltH", + "Y2YImkLOY5ElmvnfUSX3xNZGZk4B/5Dv7Ad31JsitktGcD691QDOIOhaVb/ytdtko1Epbf2ukNoXg1pe", + "TBjjOz/jqIK4fQyMdCiO5xjsXb/YkV2nFnBm03ez+ZvrfzMlGhTVg7t1ndWub1244+5Obzd5FU05q+Wa", + "Ni5o8Qqi0h+NjwdjyF0dTEHGyXxu6wDe2XQocx+vqSYs2Wl6xjgM+q7irSqE6YMASO8YLvs2dnn7tfDu", + "gXLJoqVzFXXzVvj4nUAYzY0znutRP61yd0c1T7x257K/A26Ru4X2LmrERYS51y8bMHKooot+jpxgmks4", + "SfELjbQq6wOYmFDns5GQaWaGSZEtlkD4hPu26wXjhYRyNba50QjWrxprL/m2kAnRVOkJzxOgq2nUeQUb", + "BECfZ0lieDvltu7LhJvRHBu0e4ngCkA4Q9c2fMecAWITG6dppCe8mt0IxPycUmkUG7KgQxCcYjueFUmG", + "cGyXtEOZmnAjMIoMDB9igz0gycpJoNkGzilXxAwkiVjk0e8T3s+4//ofNLaT+wpvxsTGfpM+/eT1m7MT", + "TPuY8Dx+/tXZydhlhyXoK3vz05vT/4kA6+e2wpEx/lMaH1GDiYPhhCsDFKY3I6xKR2ObTYJXymIz6dBy", + "WHNRUiRTFqO9ZSulTri/i6KUZnHNfbpK9QaEXlK5ZooOrE/B6I7mU2PKYDpTtKTROYhMp5nGpzKzJaBf", + "U6F81V0z1pXqQQQzAJ8ZEjF61//39PF39uQIKSvJmcL7ynhKFoyjOYvyejzhp1u9N5pT5sGmqLWYTUW9", + "qttUg7TYS8chPPa36PKD8HaBxepFJQWphNFEWnPSZRJlWNoPU5RwC3dbLSpu6T3lqrUpaHH3hk9B39Uf", + "sHzqQSk1pgSbBw7/7mKSz53y3xR0XIOyIVYD6SGsCN+UuMgFo+vQe2JdeLkqGVv5rmEvTdhAQEaXSUm5", + "nlox8dJ6WCyXsylGQ/CscrYBzz6Lcp7gSv8u2WLp/72iMctW/r8SsXb/nPCMG1PRMt2EKD1FZmiNSMPl", + "x/CFacPTa8QYiRWd8NyxyvhoRVdGDFj5YvmqFTIvnKRa5n9B9ll67x2CNovAnBhROiPROZYCMmzdzMMc", + "E/aS3Kzrb8fSPkhqawW5Oif4tWFEIXZkBHvOj2idHSnrYM8/+abCnCYcKxqWhNHYiuCpF42u9nMq0Kti", + "NjeEVNIROpPM0ljqbR8R0ML73yLKNWRM3wDz/0C+OpEvEa+NuDxxpR9d4ceHx8c/v/AvHPDwuIlNtzjb", + "HobqQF6/ILrbAqV09W3S5G212Ghd27qXF+3yAuMNiDc5HGtONr64QVmMSJc5v1NMePRpFhKvsV10pOvo", + "e043yteaLVlCNVmCJZVLieM8w1BPMXekuiKpo09KomVZmpQZq2G6b1B/zrk3Cs4l8QryjFIOztYZT/j3", + "eKVoPxmBmrLo3Kxa+rSs1TK63rN41D6a8NsCxrfipb0u3bE4VyvN56MsEhjjpbhYe/3O8rmzVa3uHBNA", + "Q6MAbIn8EJJWi1sRY3KLDlzA1z9v5AFfSguIdfGETWKSYuVYP4N8vlUtezjh3BgLfkjsNFvHv8BIUXlB", + "kmFe5SElmbIxjWrC+7m1W35Nf2B9CX5VW6GJcqO/xeWIAVSv5mwxGMOrwkMqMo3ODvs1Hkk6VdiCzvka", + "XDF5AjxLEjCnmKLzhWgYOb5DOKD34KDy7VV6OvO38IfiEvmpQsGX/gac0oPOxpI/654X9H60mAhCgntR", + "c5CpMIVPPKdPT5KIfgWO919lWti/aZbQLiZkt0L2xrbIJM3LzV+ukn3ugByC8z8OvZ/PVawfw2uyUZYy", + "HR27ldEnQ2YKn15zDiUl2bwA90g74SSKslWGTtViUIwVyG0dD1gIs+W5kGsi44Y+NG2l66vo31C5/gbM", + "oT9YOfwQWBur4f9e6uDfSWZiQVgS457EEeuRQQtOiy+DXKQoU/4cnTjNGUYn6GfJnwYYH6VSREUc/YpE", + "S8ap3PiQHyZiFkEiRAqZMvp6vwgnHM0lpfDl5PNoZkwBVPlTITU8ejQYmo8VvgZoYbX8PJpv6BJ9i1JU", + "c0nVEkP5Ej2GWn9b29rOEOq2iVCqk48nb0p9sghdjD5BMN1gst8V9fB9+OjbTj18b6DqP4LwFK8sWPff", + "BYHZ3w+sFndvmdxei6tXjiVYHZFZoABTeWA84zBP2GJZZ2lnGx4tpeAiUyD4KKYrS+6l+vfFzNWoyQYO", + "FzMVGUVn81xmvJm5fUopt29vZ2c/gKJ4bUAWhHFnxuERMoXeWq3AxdbHE14wtaGtdY0+60QoGo8U1W7D", + "Myym0hdqJGlCiaJDyDBrAUsYMD4XQ4jnw1I2w4JqyudCRnQIhIysZ39oZCRdkyQZYMct5KtmQfsQqYaQ", + "pYpK7fw71sKZmunhAcSUG5aT4Fstwmgs1PT/MrZXkq04JirkQC2VGx9Cms0SppZmMXpBuZ5laoxxOw66", + "NLaMma5Y6b19nAN/nL+KTzjJYqYBp3FM2dlhyJeLT1rYsV92c5rxe058iIZ2hiB/x+ciqJx5+DomDP/1", + "v/6Pe4rBqN4Y7PdvSaTV75ND33oW6TaLfnqTXeRtzFLRogiv2PC+mJHEKJ6l5BHH68C+Vd546meBjcol", + "gRqwrQ2ZeU+x+WGrY2pYmHw6gznjCypTybiGnN90FylFc9NWoxsFRtGzEauheqdJ6blekxk8gL8Y8YBD", + "bNun7zf565dXdkVKuQteLuJWHuQ9JAcv4N+c9YzJNEb1tR/aACIzb6ChK4kPauHq7Oc3BShukA3XjN9l", + "HvgfsH/nJFE0n2omREIJv2YGm0PlPVO6tQ2pqjfuuCuNWn8P9m/tma0UQVtgZWOx17NshrpKsJ9yKTnu", + "gcwSOoZPnCIBTniJ+MwgT32lj1HPTcTaNrMrZnkBZMLjzEKN5nT95Pg736Tdd2F34QCVygvGhpbjCd8m", + "YfyqZN/u1Ye5QsW/4yDhUhPmW0md7tCVOdC2+3emOW1h3Z1kEjdfcxZFc8EAgnVnvRVsmwQl292eLUBJ", + "kJFBH98L14RdUDkwWg9pVVFURHhLwT2bc24DiQk3Viv07Tudj1hOxGImxLnRGgbWsuPYIEjZiLIfPrw6", + "GSm24O6VEH4RM2RZayHPMQyWRplZ4YIR+DPliozBB7E9On5Uav6MX7M4tzDsf2tFkzkyUlUocS/AWZFM", + "8AlPsLUn4/VAhqNKnyyzdTMN5yLjUa4wOjMWjB1rGLANVyAIiUanhWuAJeSEuy5PW/5GKNyN1WAtW26p", + "9PaIxq45bBtjPovIv4x9e3W2j4HaaWa7dcXh50yH9L6ouMESg8EOyeN7k/b36XVE5rGbfn2TthoLfmNv", + "HwgvmYuVblKEd7cSnVOL8bno1FKjnJC65bsrJ5As2IXRQ9HBBp9Fio+kNqw35EeDfomFGn484Z8/nX2B", + "RicpqrXmGzNlJXpjMJ7wJ8dPnOuQCz3FiwY2hz4ZFF5STDxE4TyE/mxQBCGbX1SR0hkPzVr9qPSpE5mz", + "TENu9k84ho8JjTS7odYdZTmmsD37zY/Wz1rMQjGsxAggxAbKY3xtrD8Ele6pxdAt+cv+AFEf7d6/95jJ", + "BNWB9/EeQf+ZVUE/nQIXUPKairXB0y0Vj8SlVLHS+DmJnJ4YfLBlXNMkYQuD0UeouHTr0mO7g1pXu/3L", + "pzN4V5oMIpEkNNKo0vgCJ4a4OF0nG1RrjRErpFZDSEl0Tha+NiGGBaM3bsJ9ZydbdwlOMqmEBJfCZLRX", + "wSGmGpOvihQBG3o94bMNuHIMQ7vVaSRiWkQdDwEb8h5lXLOk7KwSalSGTAP1ls/7xsKuU8m2okTEpR1U", + "xal6DRrTsye7FKaGqT2QKhNTnq16z//aY5ZdJWLdG/ZsNkdv2FuyheFUPvOj93P3xa60MTLe55XNFiHS", + "XWfLtUe3WrBjG40/Y+PlgHPRkvul+jLf4YZP6PXb4mNlTtfMO62BWCkcvb9eVp6xyMMqsq1QP7Pvn9Cg", + "LQU0JSND2HzCuaidDLvvGgUozVU9zHGyIgb6uR404S2KEGzpQYPLsdIz7Af8R6gSt32qoE6kdB7AeK/+", + "hNSfPLxzW/NB6GHqfQW38y8aNZ+ERZSr1v7S792Qa8QQtwQiR1sKhRtX6pVdgODfqQ0j8dH0SXks9DWj", + "cghzSrRVpP6eCU2MjoVRH9UoYC40m7uTqCPD+jhNWivVfix9ceLHXyPAAus1PYW5n+vVY9vF1VshZyyO", + "KS+9Rbd/4coH/1gvF1wVK2XIggcs9BWNJMWYn5gYHbatVFR5ig7lZAOQuqbisoGVbqfNX+jILYjhfXTu", + "FkqXcCnl5sYRLK/zGkKyrgi1XRIlzAw6ViULY98d7C13O1eWd0q7zJXtLkh2Z27h+JZo/LA7dvpO+xcf", + "hX5bxFddBVL4Ml4hnAgwqX0lRUspr9vEkxuRR7fTLa8jrvryss1XfVPy6JYQP2//dmMC7LmmVm8K609f", + "qNJ3WYJ9wff8hEoNMU3YRVE64Y+LJGeUx0BAbbheUs0i0AUQfH0175reA200rRmGkqJXupMH59SOhYTN", + "JJGb5747OOUGm2gMdjIfijfhGIvX1mXerd7gJnHrXaeBZZfY1ZbDhh1VDn/fS2hfV2MdTRT0yyGbgxBa", + "5up3K27mz8j2K5htgMVDsMVNGV/Yx1esOwd/Ovv00ZYgwrSJy6Hm7RUivmoKaMf6e2S/Uz5De2U76uMK", + "ntNDH4PEmFZlIvBJrkG6e+6JtSX0zVYCxCqZgnE9Yhxfmrby423WuXfXxUSTCe/XC7AV5exdsc4H4N/V", + "hr58Tcb1ELRI7TtpXlbJhtXhyRQwjYU7OeStsx0QbGTwh58+T7g/mwLBExuZYcPssK6Hq++H4SmuxKly", + "fMIVyqn3327hFJ+x4K35+d89QA83AhxXELNfaIQEe53OpXa2UOfl9wxinw6WHheAwNuEUl3OYnmTY+KZ", + "G18hT1Gtbrgtl4QtRndt4uJVvGIcV2ltBSQSegf6mBlwNfYxm2UsMVwLpINZkwJdnaVyFc+tU7W9MRRC", + "y7mIr8cFcJIpLVZmnVvtelZso7XCKo5CqN9aC7TP+f36t7cbD7JEEmExaHJOeZOLOypgtQtBtx0C3Uqh", + "Whvuz0Xd0FxZKOU4F/XLJJ1TSXlE1YQTNFElXdGYWSeGO8IwD3IzM46cyQQzKdbKlqiwgfVYjdnNh5qB", + "jZ7Pqzzjqx2LBkNXaJTGECWMcj1SLKa5VDZzbanv5vBNynt2zUzSLND0TvfF11W9NyRr3Dq/xh2GpEGR", + "Er567ELUwbfxcvnbNsdxvmCFbPKUj53+kCLLw7fEKb+/VzMSh1hYl/GFrTKApdL8V7FkSTKKxZoD0Y40", + "oE+/ppbAfFt3RamhS4vvajD20X7mKot0ie2gwZmP2J4Snee9hWjG02SIZs4QKnsG/N1kT6hH5RCzhztD", + "zLaqR+UgAjEvdasT6zzsXVHKoX/69uTx48ffDV6AWDF7L5pIbW4Oy33inTdUlwrE1nUK1btO+99cbBuv", + "clmhGLPvEfZSIXH3rK6J1d3h0rvV5I0t191BPLazcy8i3GGg5ba+qaht4uGy8rOEfqMgzqQx+if8gsqY", + "Rb5QANEWgfu++EsRE11RbdQQcySm9ILFRilxPSskWW+1qfj46QswnjBOY1hSSV/AHP0uTGNvCyuEbLAg", + "BT9fKXkiyIZtEOEuPvxHcDuac9hmVU2MBy8sdkPuGccdYhyY9tfEOD65Tg7flOj0AeTdXmwdHMNAhCOf", + "nDguw0aOhIpI0sJMeEwlOgbNRUqHXaT0dvDp7OTVe3g4Ph4/g1dKUaVWlGuwxdXUhMciyvAv2Ktizji+", + "LDwAMVNUXlhNy9P9YIitUrjSMotsVxyxsoqfY1B2fXRaFqm93yhseyM1lXkaLxZ5GtmKlhNOpGZzUmdr", + "Dcykk06Hx/69M5NP5vJfuwsKYWz75aI//J7H/H54zCtYL0VSImK+g3zBU+9lWAy6FI7+af7vXfzbkWdb", + "OzUYfImpqCeFPuCyYM3SSO5ObZlwK/iG7unSsBEsF2b4aCRWNqPWaCQK+u6/h3lRsQm3asoQ6FemwWaB", + "0a8p5o0dkUhnJBnYOnNV7cdoO8y218LSo1KIOczokrk65W53Q9+dyz6g2FZQFYCZuSd8kefT5hjV9xkb", + "I9eCslbASWmsdezhg9VNB3uws9MsoW/8xdxgwn91UosiXbP8Hz19dptV7LbA1uK0MhIqH3bPL+8avxTO", + "7GjTzdw1oqMswIYshZY8UinZJILEgyvknN10tRLbdM/WItORWFGntmnCY5KYUSXeP+EtzL9FdRsMbeJu", + "VWWDPTS2S3nWDP3dmCb2B+BZ9wrfvzwDKzODa1T/sG5Dpbz7ke1w0vYKb6s9VAp1229uqiw4rtb+CGvD", + "cexZsKQx+tEVqGw28iEmt4qUCMMd70F5JYhKFfqV4Ezb6jQYkZQfckbOaTxiPD8tZgpk4bqjrnC+9em7", + "OYag2IITV76/VLk/70Ce13LQAk6pkZzlgs72UGPXI8cmW8dj+N6gum2xemG7UGN9Z1cvm/DYgAUb3hK5", + "sUVbMj0S85HEbO0LkmR5E0B4cnwc6M1TBlAwUilrx9prCFrYXuiGExaadtCQr2CxqJq7/DsqCeAIakds", + "kktO6EBPO/mk9bXvxyfP7Dc3dOlutQDkPlAtWaTuSKb6FfDCEq9aubM9qDQYmyckeKV5Sattudf8Mu76", + "jtC4qM1jCQz6QsJry32Lsj1r7GXObXkuW7lqAJ/f/3iGk21x7ZKMsg2/Kfz4Dqs2gETjAQhMDAZRfJ/1", + "n016QOZzIWM8b9FOSRrGOtKSpeMJ/8DMzagy6ywXdHDtzeDC9kKsc9kcWO1t0Cze10BznUhfW+pfSTP4", + "dFYqTZ5XYLsG3QD6vrHfVNEIHj87Ph6Pnx0/+fb4eAiSaDp1TYgfjsdPzd9sJaep4FN8RZ1ie9xIg6ut", + "PSyT53SRiBlJJtz9OBjDm2aNAhgv+gshw5/wUjVP+6hZ4ggFWFJmIJyl+dmsbpErI1qkIOY2PIx+1aBZ", + "dG4LXQlYCj2SqPI4LQk4pTF2Jz2ETnKNJEQnV6+O1Fe5YV0kuPwfWhGxx+ioj3Si4nbxpdaUpi2Nazgd", + "ifkcVoRnJAEcDZmyDmyD+pPeacaBi3VVjvg6uGdUa8YXtl7UGN74Mr/V/i3wi5jZAnUcS2Pa6o1G9vFR", + "WRpj2oHrop03mInzJjFTouHdGXz88f378YT/gIN9mFIxCg2Kj5++gKQjX5HVPSyilx+IEZo6Wo6ydGid", + "EoB9cmLzRam5Q1Fz2OoXGdcg5hOOh8lnzkUxGEmMVyJdT0LLdg7kA3n5xJxGzvAqb4IacaU2cYkDDMwx", + "4fzWPTn7kFZepjWAo4iiNrImbyKL6AZ1PAwRXaVW2EFqY7m80bbmWP51wrHk9CWUxwl3KHuA8jjhJe0R", + "AspjWRlv0hoDCmar4rgNnN4NlfX6l1Qfq6W2blCDNArkt8+eBPTHR+Zv2+oh1LTDCe+oHkJdO5zwfdRD", + "qGiHE56rh7bo3v76YUeKyFXEBoq4ei0xsNANK4pNO7jXFUu6YieSDUkuFRF+mMQ6iwjfllT4qlESUBN+", + "oHvDCKgJv7R7AwMr2VeMI7GZt57lWPdzuMf9Nwo0o3LCExKb9fv4FpomGKAZMcyj+m9Pnrvb8q5yq1Cm", + "ImHRBuYsoYODesZbCi/A27vmWIh/PRmXX3i9Rn9BOVcn5Mbw3iLRinGbPy4pnLx/9eHzm9fYoXvC//p0", + "CI++/fb4Z9vjJRd9RQPvh8fHP5sflth4HVW3XzC6acIxfsEjCNBoKajvJGq7gFuRNXhhn2SuXERKio2U", + "/aIWblvekwlH+fjsWKELpRwq0IkucslXo4url3ilBW5Y0tVXDku4vrvXwb+krNuLfBsFnh84SiW9YHTd", + "IvpIPMKgv1QKQ3FYlH5uM9GK5ad+QjBmomMAX/WEP3oCS5FJBf2Pn74AgVhuRjLjuf08eI6SyYyBOKO+", + "GEReEjp3suQLKCPYXJikEJwqjUvlO4in5oRD+7n3XDj3Su4xyah9pMW0Lyv5CLdJcxDTVC8vKbjO3GY+", + "O/BeM9HUlwtGWzj4Fff4O5BYj54sDS6siYxrCGh5bRj9G9H+gkhGZm1J0p84Bcq13GBovx/vHv+VNgeI", + "XbqpzVm2wbUJ9sv3idLQP8dsVbVkqYKVkLSoPZBrDijFiuTnCc8UVS8g45mNs3WC0uhUCSqcxsYj0dJt", + "LyJSMt9UrD67JQ9f3xxpUrKYWr1UUfc7EodN6maxMkAl8zkKVVtnpSARi/WZpNOVfTeEFZE2t1XIBeHs", + "H4gvI5XSiM1ZZDTFiC5FYsR+rq7yvlQblYjFVNKV0HRqe88OAXvsbKZcp9NUiGQIM8I5lVNNv+oBZj1M", + "eNG0Ri1FlsRAkjXZKLAYv7c4rVDrTzlaXDOd5gu1KpuIDyNEgxxhQQnpYqsxfO/uk26euR2VznOeZ3GP", + "NF2liZFoxRnR55gTiEE+j7ktSuePX0BSxDergL1Fn6XH+BVJoU9mymjvBnCIMFhST4RJRw3G4Psj5uRv", + "v+wbWiw9EBh90LMEe8gBkqxXTe15nhwfm+99VTIxn1MMR53wc7p5UZwQ6N8zkrh9bfELnDiWIjUKraEH", + "p6KyFW19GLQvf9Z6I3oJ1h3jwv3LfCxvK7iiclHBPNcoGtVX13bvMrprldiuR331a3wq0OfmNdmGTWzH", + "2ZfKblTwFtO91kQVPoc/toZ7asm4KrtyGiyxgpJ018KwlLaQo1ef332xg66zSFLKcJHGvHrz48GV+rf5", + "6qvP78AeHfp50qVR01+AL7nPDZ9zIYLt5ZRxog4V9z0kr4lwPQxvtaJRdRNx8126QkYvbNEbsnZwB6Yg", + "lRTlDXoUsFSH802YC7qhMrbB6kK8wBvXtTbfs8cTEDyiQ8xvx+3uxpvtMkQWMTsW0i8h1d2oPXxKL8Q5", + "jaHPfAtXPbj0DdhJKzewE7C+/H0JsplC4DQzux9xwDUyOlxgV/VcM+gOFIMz0GosBpc5SDXdQenjNp5Y", + "APzqGaKZ+1aZodnAznu+rYpuP9m4eXQHyP+fvatpjRAGon9FcuqhYKEeirf2L5T2UjyIhlaIiSRG8OB/", + "L5kkRvyKFqou7P3tamZeJoNh3usvHbSf4gV458rijMqbQvi4N62rQNmNZVWl5jzVEJ8pyZFe94qjS9NF", + "785Rd2NW/J4k50b+6dDNf61sWn+RLXmczH25zWWEPlMhim+6LvQJMVLoVw2+XU8RuxK9kF3nzVwPxQgO", + "dABPOBns5xuuBTfHUhLwWnBNTnBQMz9hQG3T0GGVMpLuIs2Hhd9pM6ANxyVrJv33qJtWEJtCmNv+cxIb", + "zDViuan+NJB/LLbmEWv11kCcxlLv1p/jirDWDEY/IoEzCcbG8VcyjNqbLEhu1+v+5iGljLYl016J8Hve", + "WCKOXBpZlpIgx7D1zHSt5ATF6KeuKxGHIVEIUMR+iaJn1CXdbwAAAP//", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/server/api_authpolicy_test.go b/internal/server/api_authpolicy_test.go new file mode 100644 index 00000000..948a46e1 --- /dev/null +++ b/internal/server/api_authpolicy_test.go @@ -0,0 +1,136 @@ +// @spec api-auth-policy +// +// Read + replace the workspace authentication policy under +// /api/v1/auth-policy: per-verb RBAC, bounds rejection, audit on update. + +package server + +import ( + "encoding/json" + "net/http" + "os" + "strings" + "testing" + + "github.com/Hanalyx/openwatch/internal/auth" + "github.com/Hanalyx/openwatch/internal/server/api" +) + +// @ac AC-01 +func TestAuthPolicy_GetRBAC(t *testing.T) { + t.Run("api-auth-policy/AC-01", func(t *testing.T) { + url, _ := freshAPIServer(t) + // Anonymous denied. + if r := doReq(t, asRole(t, "GET", url+"/api/v1/auth-policy", "", nil)); r.StatusCode != http.StatusUnauthorized && r.StatusCode != http.StatusForbidden { + t.Errorf("anon get = %d, want 401/403", r.StatusCode) + } + // Viewer lacks system:auth_policy_read → 403. + if r := doReq(t, asRole(t, "GET", url+"/api/v1/auth-policy", auth.RoleViewer, nil)); r.StatusCode != http.StatusForbidden { + t.Errorf("viewer get = %d, want 403", r.StatusCode) + } + // Admin → 200 with the seeded defaults. + r := doReq(t, asRole(t, "GET", url+"/api/v1/auth-policy", auth.RoleAdmin, nil)) + if r.StatusCode != http.StatusOK { + t.Fatalf("admin get = %d, want 200", r.StatusCode) + } + var p api.AuthPolicy + if err := json.NewDecoder(r.Body).Decode(&p); err != nil { + t.Fatalf("decode: %v", err) + } + if p.RequireMfa { + t.Errorf("default require_mfa = true, want false") + } + if p.SessionIdleTimeoutSeconds != 900 || p.SessionAbsoluteTimeoutSeconds != 43200 { + t.Errorf("default windows = (%d,%d), want (900,43200)", + p.SessionIdleTimeoutSeconds, p.SessionAbsoluteTimeoutSeconds) + } + }) +} + +// @ac AC-02 +func TestAuthPolicy_PutRBACAndRoundTrip(t *testing.T) { + t.Run("api-auth-policy/AC-02", func(t *testing.T) { + url, _ := freshAPIServer(t) + body := map[string]any{ + "require_mfa": true, + "session_idle_timeout_seconds": 1800, + "session_absolute_timeout_seconds": 86400, + } + // Viewer lacks system:auth_policy_write → 403. + if r := doReq(t, asRole(t, "PUT", url+"/api/v1/auth-policy", auth.RoleViewer, body)); r.StatusCode != http.StatusForbidden { + t.Errorf("viewer put = %d, want 403", r.StatusCode) + } + // Admin → 200 with the updated values. + r := doReq(t, asRole(t, "PUT", url+"/api/v1/auth-policy", auth.RoleAdmin, body)) + if r.StatusCode != http.StatusOK { + t.Fatalf("admin put = %d, want 200", r.StatusCode) + } + var p api.AuthPolicy + if err := json.NewDecoder(r.Body).Decode(&p); err != nil { + t.Fatalf("decode: %v", err) + } + if !p.RequireMfa || p.SessionIdleTimeoutSeconds != 1800 || p.SessionAbsoluteTimeoutSeconds != 86400 { + t.Errorf("put echo = %+v, want require_mfa true / 1800 / 86400", p) + } + // A subsequent GET returns the new values. + gr := doReq(t, asRole(t, "GET", url+"/api/v1/auth-policy", auth.RoleAdmin, nil)) + var p2 api.AuthPolicy + if err := json.NewDecoder(gr.Body).Decode(&p2); err != nil { + t.Fatalf("decode get: %v", err) + } + if !p2.RequireMfa || p2.SessionIdleTimeoutSeconds != 1800 { + t.Errorf("get after put = %+v, want persisted values", p2) + } + }) +} + +// @ac AC-03 +func TestAuthPolicy_PutRejectsOutOfBounds(t *testing.T) { + t.Run("api-auth-policy/AC-03", func(t *testing.T) { + url, _ := freshAPIServer(t) + // Idle below the 300 s floor. + tooShort := map[string]any{ + "require_mfa": false, + "session_idle_timeout_seconds": 60, + "session_absolute_timeout_seconds": 43200, + } + r := doReq(t, asRole(t, "PUT", url+"/api/v1/auth-policy", auth.RoleAdmin, tooShort)) + if r.StatusCode != http.StatusBadRequest { + t.Fatalf("idle-too-short put = %d, want 400", r.StatusCode) + } + if b := readBody(t, r); !strings.Contains(b, "auth_policy.invalid") { + t.Errorf("error code missing auth_policy.invalid: %s", b) + } + // Absolute shorter than idle. + absLtIdle := map[string]any{ + "require_mfa": false, + "session_idle_timeout_seconds": 7200, + "session_absolute_timeout_seconds": 3600, + } + if r := doReq(t, asRole(t, "PUT", url+"/api/v1/auth-policy", auth.RoleAdmin, absLtIdle)); r.StatusCode != http.StatusBadRequest { + t.Errorf("absolute- + Read + replace the workspace authentication policy under + /api/v1/auth-policy: GET (system:auth_policy_read) and PUT + (system:auth_policy_write). Backed by internal/authpolicy + (system-auth-policy). + description: >- + Two operations on a single workspace policy. GET returns the current + require-MFA flag and session timeout windows (seconds). PUT replaces + the whole policy, validating the window bounds server-side, and audits + the change. The endpoints are always wired (the service only wraps the + pool). + related_specs: + - system-auth-policy + - system-rbac + + objective: + summary: >- + Lock the route contract, the per-verb RBAC, the bounds rejection, and + the audit emission. + scope: + includes: + - "GET /api/v1/auth-policy (system:auth_policy_read)" + - "PUT /api/v1/auth-policy (system:auth_policy_write) — full replace, bounds-validated" + - "auth.policy.updated audit on a successful PUT" + excludes: + - "SSO provider configuration endpoints (separate slice)" + + constraints: + - id: C-01 + description: >- + GET MUST enforce system:auth_policy_read and PUT MUST enforce + system:auth_policy_write via auth.EnforcePermission before any work. + A caller lacking the permission gets 403; an anonymous caller is + denied. + type: security + enforcement: error + - id: C-02 + description: >- + PUT MUST reject an out-of-bounds window with 400 (auth_policy.invalid) + rather than persisting it. A successful PUT MUST emit an + auth.policy.updated audit event carrying the new values. + type: security + enforcement: error + + acceptance_criteria: + - id: AC-01 + description: GET /auth-policy returns 200 with require_mfa + both timeout-seconds fields for a caller with system:auth_policy_read; a caller without it gets 403. + priority: critical + references_constraints: [C-01] + - id: AC-02 + description: PUT /auth-policy with valid values returns 200 with the updated policy and gates on system:auth_policy_write (a caller without it gets 403); a subsequent GET returns the new values. + priority: critical + references_constraints: [C-01] + - id: AC-03 + description: PUT /auth-policy with an out-of-bounds window (idle below 300 s, or absolute below idle) returns 400 with code auth_policy.invalid and does not change the stored policy. + priority: high + references_constraints: [C-02] + - id: AC-04 + description: "Source inspection — PutAuthPolicy emits audit.AuthPolicyUpdated with the require_mfa + timeout values on the success path." + priority: high + references_constraints: [C-02] diff --git a/specs/frontend/settings.spec.yaml b/specs/frontend/settings.spec.yaml index cf8e2d46..da743180 100644 --- a/specs/frontend/settings.spec.yaml +++ b/specs/frontend/settings.spec.yaml @@ -1,7 +1,7 @@ spec: id: frontend-settings title: Settings — two-pane shell with 11 sub-pages - version: "1.6.0" + version: "1.7.0" status: draft tier: 2 @@ -63,7 +63,7 @@ spec: - "Audit log — filterable, cursor-paginated GET /api/v1/audit/events, audit:read gated, read-only (v1.3.0)" - "About — version from GET /api/v1/version + license state from GET /api/v1/license (v1.3.0)" - "Notifications — Slack/webhook channel CRUD + test over /api/v1/notifications/channels, notification:read gated, secrets write-only (v1.5.0)" - - "Security & auth — live API-tokens section (list/create/revoke over /api/v1/tokens, secret shown once); SSO + auth-policy stay Backend Pending (v1.6.0)" + - "Security & auth — live API-tokens section (list/create/revoke over /api/v1/tokens, secret shown once) + live authentication-policy section (require-MFA toggle + idle/absolute session timeouts over /api/v1/auth-policy, system:auth_policy_* gated); SSO stays Backend Pending (v1.7.0)" - "Integrations — structural shell with Backend Pending banner" - "All control widgets themed via --ow-* tokens" excludes: @@ -145,8 +145,8 @@ spec: The remaining stubbed page (Integrations) MUST render a BackendPendingBanner identifying the slice that unblocks it. Every interactive control on it MUST be disabled. (Security & auth keeps - BackendPendingBanner only on its SSO + auth-policy sections; its - API-tokens section is live per C-16.) + BackendPendingBanner only on its SSO section; its API-tokens and + authentication-policy sections are live per C-16.) type: technical enforcement: error - id: C-10 @@ -213,7 +213,11 @@ spec: and a "won't be shown again" warning, Revoke via DELETE /api/v1/tokens/{id} (token:delete); each mutation invalidates ['api-tokens']. The list MUST render only the non-secret prefix. - SSO + authentication-policy sections stay BackendPendingBanner. + v1.7.0 — it MUST also render a live authentication-policy section: + GET /api/v1/auth-policy keyed ['auth-policy'] (system:auth_policy_read), + a require-MFA Toggle + idle/absolute timeout Steppers, Save via PUT + /api/v1/auth-policy (system:auth_policy_write) invalidating + ['auth-policy']. Only the SSO section stays BackendPendingBanner. type: security enforcement: error @@ -311,6 +315,11 @@ spec: priority: high references_constraints: [C-15] - id: AC-24 - description: "v1.6.0 source-inspection: SecurityPage gates the page on hasPermission('admin') (ForbiddenPage otherwise); the API-tokens section lists from GET /api/v1/tokens keyed ['api-tokens'], Create calls api.POST '/api/v1/tokens' and renders the returned token once (a copy control + a not-shown-again warning), Revoke calls api.DELETE '/api/v1/tokens/{id}', and both mutations invalidate ['api-tokens']; the list renders token.prefix and references no raw token/secret list field; write/delete controls gate on token:write/token:delete; SSO + auth-policy sections still render BackendPendingBanner." + description: "v1.6.0 source-inspection: SecurityPage gates the page on hasPermission('admin') (ForbiddenPage otherwise); the API-tokens section lists from GET /api/v1/tokens keyed ['api-tokens'], Create calls api.POST '/api/v1/tokens' and renders the returned token once (a copy control + a not-shown-again warning), Revoke calls api.DELETE '/api/v1/tokens/{id}', and both mutations invalidate ['api-tokens']; the list renders token.prefix and references no raw token/secret list field; write/delete controls gate on token:write/token:delete; the SSO section still renders BackendPendingBanner." + priority: high + references_constraints: [C-16] + + - id: AC-25 + description: "v1.7.0 source-inspection: SecurityPage's authentication-policy section loads GET /api/v1/auth-policy keyed ['auth-policy'] (gated on system:auth_policy_read), renders a require-MFA Toggle plus idle/absolute timeout Steppers seeded from the policy, and Saves via api.PUT '/api/v1/auth-policy' (gated on system:auth_policy_write) invalidating ['auth-policy']; LoginPage reads mfa_enrollment_required from the login response and routes to /settings/profile when it is set." priority: high references_constraints: [C-16] diff --git a/specs/system/auth-identity.spec.yaml b/specs/system/auth-identity.spec.yaml index 944ede84..df7cb2c3 100644 --- a/specs/system/auth-identity.spec.yaml +++ b/specs/system/auth-identity.spec.yaml @@ -65,7 +65,13 @@ spec: type: security enforcement: error - id: C-06 - description: Session inactivity timeout MUST be 15 minutes; absolute timeout MUST be 12 hours + description: >- + The DEFAULT session inactivity timeout MUST be 15 minutes and the + default absolute timeout MUST be 12 hours + (identity.DefaultSessionInactivityWindow / DefaultSessionAbsoluteWindow). + These windows are overridable at runtime by the workspace + authentication policy (see system-auth-policy); when no policy is + installed the defaults apply, preserving historical behaviour. type: security enforcement: error - id: C-07 diff --git a/specs/system/auth-policy.spec.yaml b/specs/system/auth-policy.spec.yaml new file mode 100644 index 00000000..8a67674a --- /dev/null +++ b/specs/system/auth-policy.spec.yaml @@ -0,0 +1,103 @@ +spec: + id: system-auth-policy + title: Workspace authentication policy — require-MFA + session timeout windows + version: "1.0.0" + status: approved + tier: 1 + + context: + system: openwatch-go + feature: >- + The workspace authentication policy is a single row (migration 0033) + holding the require-MFA flag and the session idle/absolute timeout + windows. internal/authpolicy owns the store; it primes the identity + package's active session windows so a change takes effect for newly + issued sessions and the rolling idle extension immediately. + description: >- + Promoting the session windows from hard-coded identity constants to + data lets a security admin tune them from Settings -> Security without + a redeploy. The defaults match the historical constants (15-minute + idle, 12-hour absolute), so the promotion is behaviour-preserving + until an admin changes the policy. require_mfa is soft-enforced at + login: a password-valid but non-enrolled user is still issued a + session (so they can reach the auth-gated enrollment endpoint) but the + login response flags mfa_enrollment_required. + related_specs: + - system-auth-identity + - api-auth-policy + - system-rbac + + objective: + summary: >- + Lock the singleton store, the window bounds validation, the + identity-window priming, and the soft require-MFA login signal. + scope: + includes: + - "auth_policy migration 0033 (singleton row, seeded defaults)" + - "Get returns the stored policy; Update validates + persists + re-primes identity windows" + - "identity.Windows are runtime-swappable (SetSessionWindows / CurrentWindows); defaults preserve 15m/12h" + - "Soft require-MFA: login issues a session but flags mfa_enrollment_required for a non-enrolled user" + excludes: + - "SSO / external IdP policy (separate slice)" + - "Per-user policy overrides (policy is workspace-wide)" + + constraints: + - id: C-01 + description: >- + The auth_policy table MUST hold at most one row (singleton: BOOLEAN + primary key pinned to TRUE via CHECK). The migration MUST seed that + row with the behaviour-preserving defaults (require_mfa false, + 900 s idle, 43200 s absolute). + type: technical + enforcement: error + - id: C-02 + description: >- + Update MUST reject (ErrInvalidParams) an idle window outside + 5 min..24 h, an absolute window outside 1 h..30 d, or an absolute + window shorter than the idle window. A valid Update MUST persist the + values and re-prime the identity session windows so the change is + effective without a restart. + type: security + enforcement: error + - id: C-03 + description: >- + identity.SetSessionWindows MUST coerce a non-positive idle or + absolute field to its default, so a malformed policy can never + disable session expiry. CurrentWindows MUST return the defaults + (15 min / 12 h) when no policy has been installed. + type: security + enforcement: error + - id: C-04 + description: >- + require_mfa enforcement MUST be soft at login: a password-valid user + without MFA enrolled is still issued a session, and the login + response flags mfa_enrollment_required. The flag MUST be set only + when policy require_mfa is true AND the user is not enrolled. + type: security + enforcement: error + + acceptance_criteria: + - id: AC-01 + description: Get on a freshly migrated database returns the seeded defaults (require_mfa false, idle 15m, absolute 12h); the auth_policy table rejects a second row insert (singleton CHECK). + priority: critical + references_constraints: [C-01] + - id: AC-02 + description: Update with valid values persists them and a subsequent Get returns them; Update returns ErrInvalidParams for an out-of-bounds idle window, an out-of-bounds absolute window, and an absolute shorter than idle. + priority: critical + references_constraints: [C-02] + - id: AC-03 + description: "Source inspection — PostAuthLogin sets mfa_enrollment_required only when authPolicySvc.Get reports RequireMFA AND the user is not enrolled; the session/cookies are still issued in that branch (no early return)." + priority: critical + references_constraints: [C-04] + - id: AC-04 + description: "Source inspection — login soft enforcement does not block: there is no writeError/return on the require_mfa-and-not-enrolled path; the response carries the flag alongside the issued tokens." + priority: high + references_constraints: [C-04] + - id: AC-05 + description: SetSessionWindows with a zero or negative idle/absolute coerces to the default; after Update, IssueSession stamps expires_at and absolute_expires_at using the updated windows. + priority: critical + references_constraints: [C-02, C-03] + - id: AC-06 + description: "Source inspection — server.New constructs the authpolicy service and calls Prime so the identity windows reflect the stored policy from the first request; Prime failure is non-fatal (defaults remain)." + priority: high + references_constraints: [C-03] diff --git a/specter.yaml b/specter.yaml index 687d8e72..63f4373b 100644 --- a/specter.yaml +++ b/specter.yaml @@ -84,6 +84,8 @@ domains: - api-notifications - system-api-tokens - api-tokens + - system-auth-policy + - api-auth-policy settings: specs_dir: specs # Honored by `specter coverage` (not by `specter sync` — CI passes