From 5fca81531a9d1ee26050a5d48e2d5b199135aa4e Mon Sep 17 00:00:00 2001 From: Mikhail Date: Sun, 16 Aug 2026 16:56:38 +0300 Subject: [PATCH] feat: add Creator username password login --- cmd/core-runtime/main.go | 51 +++++++---- cmd/core-runtime/main_test.go | 51 +++++++++++ cmd/creator-web/server_test.go | 32 ++++++- cmd/creator-web/static/assets/console.css | 1 + cmd/creator-web/static/assets/console.js | 20 +++-- cmd/creator-web/static/index.html | 10 ++- contracts/identity/session.go | 5 ++ deploy/docker-compose.dev.yml | 2 + deploy/docker-compose.prod.yml | 2 + dev/ACTIVE_TASK.json | 6 +- .../DEV-000033-creator-username-password.json | 87 +++++++++++++++++++ docs/DEPLOYMENT.md | 2 + modules/identity/domain/session.go | 10 +++ tests/creator_auth/session_test.go | 18 ++++ tests/creator_web/verify-remote.ps1 | 16 +++- 15 files changed, 279 insertions(+), 34 deletions(-) create mode 100644 dev/tasks/DEV-000033-creator-username-password.json diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 78c077e..2ec46aa 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -12,6 +12,7 @@ import ( identitydomain "git.cin.su/m/Assistant/modules/identity/domain" situationdomain "git.cin.su/m/Assistant/modules/situation/domain" "git.cin.su/m/Assistant/runtime/health" + "io" "log" "net/http" "os" @@ -19,12 +20,15 @@ import ( "time" ) +const maxCreatorCredentialsBytes = 4 << 10 + func main() { addr := os.Getenv("LISTEN_ADDR") if addr == "" { addr = ":8080" } bootstrapToken, sessionKey := os.Getenv("CREATOR_BOOTSTRAP_TOKEN"), []byte(os.Getenv("CREATOR_SESSION_SIGNING_KEY")) + creatorUsername, creatorPassword := os.Getenv("CREATOR_USERNAME"), os.Getenv("CREATOR_PASSWORD") repository, err := postgres.OpenSituation(context.Background(), postgres.ContinuityConfig{Host: os.Getenv("POSTGRES_HOST"), Port: os.Getenv("POSTGRES_PORT"), Database: os.Getenv("POSTGRES_DATABASE"), User: os.Getenv("POSTGRES_USER"), Password: os.Getenv("POSTGRES_PASSWORD")}) if err != nil { log.Fatal(err) @@ -63,22 +67,7 @@ func main() { return situationdomain.NewResolver(bindings).Resolve(runtime) } mux.Handle("/healthz", health.Handler{Runtime: "core-runtime", SemanticGeneration: 1, ArchitectureGeneration: 1}) - mux.HandleFunc("POST /v1/creator/sessions", func(w http.ResponseWriter, r *http.Request) { - if !identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) { - http.Error(w, "creator authentication denied", http.StatusUnauthorized) - return - } - token, session, err := identitydomain.IssueCreatorSession(sessionKey, time.Now(), 8*time.Hour) - if err != nil { - http.Error(w, "creator session unavailable", http.StatusServiceUnavailable) - return - } - w.Header().Set("Content-Type", "application/json") - _ = json.NewEncoder(w).Encode(struct { - Token string - Session identity.CreatorSession - }{token, session}) - }) + mux.HandleFunc("POST /v1/creator/sessions", creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword, sessionKey, time.Now)) mux.HandleFunc("GET /v1/creator/sessions/current", func(w http.ResponseWriter, r *http.Request) { session, ok := authenticateCreator(w, r) if !ok { @@ -273,6 +262,36 @@ func main() { } } +func creatorSessionHandler(bootstrapToken, creatorUsername, creatorPassword string, sessionKey []byte, now func() time.Time) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + authenticated := identitydomain.BootstrapMatches(bootstrapToken, bearerToken(r)) + if !authenticated { + var credentials identity.CreatorCredentials + decoder := json.NewDecoder(io.LimitReader(r.Body, maxCreatorCredentialsBytes+1)) + decoder.DisallowUnknownFields() + if err := decoder.Decode(&credentials); err != nil || decoder.Decode(&struct{}{}) != io.EOF { + http.Error(w, "creator authentication denied", http.StatusUnauthorized) + return + } + authenticated = identitydomain.CreatorCredentialsMatch(creatorUsername, creatorPassword, credentials) + } + if !authenticated { + http.Error(w, "creator authentication denied", http.StatusUnauthorized) + return + } + token, session, err := identitydomain.IssueCreatorSession(sessionKey, now(), 8*time.Hour) + if err != nil { + http.Error(w, "creator session unavailable", http.StatusServiceUnavailable) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(struct { + Token string + Session identity.CreatorSession + }{token, session}) + } +} + func bearerToken(r *http.Request) string { value := r.Header.Get("Authorization") if !strings.HasPrefix(value, "Bearer ") { diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index f6a3f39..76def19 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -1,8 +1,12 @@ package main import ( + "encoding/json" + "net/http" "net/http/httptest" + "strings" "testing" + "time" ) func TestBearerTokenRequiresBearerScheme(t *testing.T) { @@ -16,3 +20,50 @@ func TestBearerTokenRequiresBearerScheme(t *testing.T) { t.Fatalf("token = %q", token) } } + +func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) { + handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) }) + request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`)) + response := httptest.NewRecorder() + handler(response, request) + if response.Code != http.StatusOK { + t.Fatalf("status = %d body=%s", response.Code, response.Body.String()) + } + var issued struct { + Token string + } + if err := json.NewDecoder(response.Body).Decode(&issued); err != nil || issued.Token == "" { + t.Fatalf("session response = %+v, err=%v", issued, err) + } +} + +func TestCreatorSessionHandlerFailsClosed(t *testing.T) { + handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), time.Now) + tests := []string{ + `{"username":"wrong","password":"password"}`, + `{"username":"m","password":"wrong"}`, + `{"username":"","password":""}`, + `{"username":"m","password":"password","actor":"attacker"}`, + `{`, + `{"username":"m","password":"password"}{}`, + } + for _, body := range tests { + request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(body)) + response := httptest.NewRecorder() + handler(response, request) + if response.Code != http.StatusUnauthorized || strings.TrimSpace(response.Body.String()) != "creator authentication denied" { + t.Fatalf("body %q: status=%d response=%q", body, response.Code, response.Body.String()) + } + } +} + +func TestCreatorSessionHandlerPreservesBootstrapRecovery(t *testing.T) { + handler := creatorSessionHandler("recovery", "", "", []byte("01234567890123456789012345678901"), time.Now) + request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", nil) + request.Header.Set("Authorization", "Bearer recovery") + response := httptest.NewRecorder() + handler(response, request) + if response.Code != http.StatusOK { + t.Fatalf("bootstrap recovery status = %d", response.Code) + } +} diff --git a/cmd/creator-web/server_test.go b/cmd/creator-web/server_test.go index a20025a..95294b8 100644 --- a/cmd/creator-web/server_test.go +++ b/cmd/creator-web/server_test.go @@ -33,6 +33,34 @@ func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) { } } +func TestGatewayForwardsCreatorCredentialsOnlyInBody(t *testing.T) { + upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/v1/creator/sessions" || r.Method != http.MethodPost { + t.Fatalf("unexpected session target %s %s", r.Method, r.URL.Path) + } + if r.Header.Get("Authorization") != "" || r.Header.Get("Content-Type") != "application/json" { + t.Fatalf("unexpected session headers Authorization=%q Content-Type=%q", r.Header.Get("Authorization"), r.Header.Get("Content-Type")) + } + body, _ := io.ReadAll(r.Body) + if string(body) != `{"username":"m","password":"secret"}` { + t.Fatalf("session body = %s", body) + } + _, _ = io.WriteString(w, `{"Token":"bounded"}`) + })) + defer upstream.Close() + handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client()) + if err != nil { + t.Fatal(err) + } + request := httptest.NewRequest(http.MethodPost, "/api/session", strings.NewReader(`{"username":"m","password":"secret"}`)) + request.Header.Set("Content-Type", "application/json") + response := httptest.NewRecorder() + handler.ServeHTTP(response, request) + if response.Code != http.StatusOK { + t.Fatalf("session response = %d %s", response.Code, response.Body.String()) + } +} + func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) { handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient) if err != nil { @@ -139,7 +167,9 @@ func TestConsoleAndSecurityHeaders(t *testing.T) { response := httptest.NewRecorder() handler.ServeHTTP(response, request) if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") || - !strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") { + !strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") || + !strings.Contains(response.Body.String(), "creator-username") || !strings.Contains(response.Body.String(), "creator-password") || + strings.Contains(response.Body.String(), "bootstrap-token") { t.Fatalf("console response = %d", response.Code) } if response.Header().Get("Content-Security-Policy") == "" { diff --git a/cmd/creator-web/static/assets/console.css b/cmd/creator-web/static/assets/console.css index 2a81a58..8ee36f1 100644 --- a/cmd/creator-web/static/assets/console.css +++ b/cmd/creator-web/static/assets/console.css @@ -42,6 +42,7 @@ button:focus-visible, input:focus-visible, textarea:focus-visible { outline: 2px .login-form input, .context-form input { width: 100%; border: 1px solid var(--line); border-radius: var(--radius); color: var(--ink); background: #0b110e; transition: border-color .18s, background .18s; } .login-form input:hover, .context-form input:hover { border-color: var(--faint); } .login-form input { height: 52px; padding: 0 50px 0 14px; font-family: var(--mono); } +.login-form input:not(#creator-password) { padding-right: 14px; margin-bottom: 16px; } .secret-row { position: relative; } .secret-row .icon-button { position: absolute; right: 7px; top: 7px; } .icon-button { width: 38px; height: 38px; display: grid; place-items: center; border: 1px solid var(--line); background: transparent; border-radius: var(--radius); cursor: pointer; transition: background .18s, border-color .18s; } diff --git a/cmd/creator-web/static/assets/console.js b/cmd/creator-web/static/assets/console.js index 6b38bbc..e78173e 100644 --- a/cmd/creator-web/static/assets/console.js +++ b/cmd/creator-web/static/assets/console.js @@ -99,18 +99,23 @@ function endSession(message = "Сессия завершена.") { loginForm.addEventListener("submit", async (event) => { event.preventDefault(); const button = byId("login-button"); - const bootstrap = byId("bootstrap-token").value.trim(); + const username = byId("creator-username").value.trim(); + const password = byId("creator-password").value; loginError.textContent = ""; button.disabled = true; - button.firstElementChild.textContent = "Проверяю ключ…"; + button.firstElementChild.textContent = "Проверяю данные…"; try { - const result = await request("/api/session", {method: "POST", headers: {Authorization: `Bearer ${bootstrap}`}}); + const result = await request("/api/session", { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({username, password}) + }); sessionToken = result.Token; sessionStorage.setItem("domovoy.creator.session", sessionToken); - byId("bootstrap-token").value = ""; + byId("creator-password").value = ""; await showConsole(); } catch (error) { - loginError.textContent = error.message === "creator authentication denied" ? "Ключ не принят." : error.message; + loginError.textContent = error.message === "creator authentication denied" ? "Логин или пароль не принят." : error.message; } finally { button.disabled = false; button.firstElementChild.textContent = "Открыть консоль"; @@ -118,9 +123,10 @@ loginForm.addEventListener("submit", async (event) => { }); byId("toggle-secret").addEventListener("click", () => { - const input = byId("bootstrap-token"); + const input = byId("creator-password"); input.type = input.type === "password" ? "text" : "password"; - byId("toggle-secret").setAttribute("aria-label", input.type === "password" ? "Показать ключ" : "Скрыть ключ"); + byId("toggle-secret").setAttribute("aria-label", input.type === "password" ? "Показать пароль" : "Скрыть пароль"); + byId("toggle-secret").setAttribute("title", input.type === "password" ? "Показать пароль" : "Скрыть пароль"); }); contextForm.addEventListener("submit", (event) => { diff --git a/cmd/creator-web/static/index.html b/cmd/creator-web/static/index.html index 12c0ea4..cbc106a 100644 --- a/cmd/creator-web/static/index.html +++ b/cmd/creator-web/static/index.html @@ -16,12 +16,14 @@

защищённый вход · creator

Домовой

-

Личная операторская консоль. Ключ используется только для выпуска ограниченной сессии и не сохраняется браузером.

+

Личная операторская консоль. Учётные данные используются только для выпуска ограниченной сессии и не сохраняются браузером.

- + + +
- -
diff --git a/contracts/identity/session.go b/contracts/identity/session.go index 8b07c7d..4256088 100644 --- a/contracts/identity/session.go +++ b/contracts/identity/session.go @@ -1,5 +1,10 @@ package identity +type CreatorCredentials struct { + Username string `json:"username"` + Password string `json:"password"` +} + type CreatorSession struct { ActorID ActorID PrincipalID PrincipalID diff --git a/deploy/docker-compose.dev.yml b/deploy/docker-compose.dev.yml index dacbf89..79c0a2a 100644 --- a/deploy/docker-compose.dev.yml +++ b/deploy/docker-compose.dev.yml @@ -44,6 +44,8 @@ services: IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD:-assistant_identity_dev_only} CREATOR_BOOTSTRAP_TOKEN: ${CREATOR_BOOTSTRAP_TOKEN:-creator_dev_only_replace_me} CREATOR_SESSION_SIGNING_KEY: ${CREATOR_SESSION_SIGNING_KEY:-creator_dev_only_signing_key_32_bytes} + CREATOR_USERNAME: ${CREATOR_USERNAME:-creator} + CREATOR_PASSWORD: ${CREATOR_PASSWORD:-creator_dev_only_replace_me} WORK_POSTGRES_USER: work_role WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD:-assistant_work_dev_only} CAPABILITY_POSTGRES_USER: capability_role diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index cdd31fa..a880494 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -44,6 +44,8 @@ services: IDENTITY_POSTGRES_PASSWORD: ${IDENTITY_DB_PASSWORD} CREATOR_BOOTSTRAP_TOKEN: ${CREATOR_BOOTSTRAP_TOKEN} CREATOR_SESSION_SIGNING_KEY: ${CREATOR_SESSION_SIGNING_KEY} + CREATOR_USERNAME: ${CREATOR_USERNAME} + CREATOR_PASSWORD: ${CREATOR_PASSWORD} WORK_POSTGRES_USER: work_role WORK_POSTGRES_PASSWORD: ${WORK_DB_PASSWORD} CAPABILITY_POSTGRES_USER: capability_role diff --git a/dev/ACTIVE_TASK.json b/dev/ACTIVE_TASK.json index f9ac58e..b74e1e7 100644 --- a/dev/ACTIVE_TASK.json +++ b/dev/ACTIVE_TASK.json @@ -1,5 +1,5 @@ { - "task_file": "dev/tasks/ARCH-000004-implement-diff-scope-guardrail.json", - "task_id": "ARCH-000004", - "task_revision": 1 + "task_file": "dev/tasks/DEV-000033-creator-username-password.json", + "task_id": "DEV-000033", + "task_revision": 2 } diff --git a/dev/tasks/DEV-000033-creator-username-password.json b/dev/tasks/DEV-000033-creator-username-password.json new file mode 100644 index 0000000..fbfe92f --- /dev/null +++ b/dev/tasks/DEV-000033-creator-username-password.json @@ -0,0 +1,87 @@ +{ + "task_id": "DEV-000033", + "task_revision": 2, + "architecture_revision": "ADR-0003 and ADR-0004", + "base_repo_revision": "111ad75deaf20a6822f5440ad232d782479ce79c", + "risk_tier": "E", + "intent": "Add a typed username/password Creator authentication flow for the single Domovoy Creator account while preserving bounded sessions, bootstrap recovery access, and all existing authority boundaries.", + "requirement_ids": ["INV-AUTH-001", "CONSTITUTION-1.3.28"], + "allowed_change_scope": { + "modules": ["identity"], + "paths": [ + "contracts/identity/session.go", + "modules/identity/domain/session.go", + "cmd/core-runtime/main.go", + "cmd/core-runtime/main_test.go", + "cmd/creator-web/server_test.go", + "cmd/creator-web/static/index.html", + "cmd/creator-web/static/assets/console.js", + "cmd/creator-web/static/assets/console.css", + "deploy/docker-compose.dev.yml", + "deploy/docker-compose.prod.yml", + "docs/DEPLOYMENT.md", + "tests/creator_auth/**", + "tests/creator_web/**", + "dev/ACTIVE_TASK.json", + "dev/tasks/DEV-000033-creator-username-password.json" + ] + }, + "forbidden_change_scope": { + "paths": [ + "architecture/**", + "ci/**", + ".gitea/**", + "security/**", + "database/**", + "modules/authority/**", + "modules/commit/**", + "modules/continuity/**", + "adapters/**" + ] + }, + "allowed_architecture_delta": { + "new_cross_module_dependencies": 0, + "new_canonical_owners": 0, + "new_privileges": 0 + }, + "contracts": { + "consume": ["contracts.identity@v1", "Creator bootstrap recovery credential"], + "may_change": ["Creator session issuance request adds typed username/password credentials"] + }, + "semantic_owner": "identity", + "canonical_state": ["no new canonical state; the single Creator identity remains domovoy-creator-actor/domovoy-creator-principal"], + "state_transitions": ["valid configured credentials issue the existing eight-hour bounded Creator session"], + "side_effects_and_privileges": ["reads externally injected Creator username and password", "does not grant Authority or accept Actor/Principal from the client"], + "persistence_and_migrations": ["none; credentials remain external runtime configuration"], + "failure_retry_restart": ["missing configuration and invalid credentials fail with the same generic denial", "credentials are not logged or persisted in browser storage", "bootstrap Bearer remains for test and recovery compatibility"], + "assumptions": [ + "The user explicitly selected username m and supplied the Creator password", + "The user requested execution, which authorizes this authentication contract change", + "ADR-0004 requires the active pointer update to be reviewed as a Tier E protected-path change" + ], + "acceptance_scenarios": [ + "valid username/password issues the existing bounded Creator session", + "wrong username, wrong password, empty fields and malformed JSON are denied without credential disclosure", + "the browser sends credentials only in the session request body and stores only the issued session token", + "Actor and Principal remain server-selected constants", + "legacy bootstrap Bearer remains accepted for recovery and remote harnesses", + "production credentials are injected outside the repository", + "desktop and mobile login remain accessible", + "remote login and current-session checks succeed through the deployed gateway" + ], + "required_tests": [ + "unit", + "adversarial authentication", + "gateway request forwarding", + "architecture preflight", + "exact diff scope", + "go vet", + "responsive browser", + "remote Docker smoke" + ], + "terminal_gap_states": [ + "BLOCKED_BY_ARCHITECTURE_GAP", + "BLOCKED_BY_SPEC_GAP", + "BLOCKED_BY_CONFLICT" + ] +} diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 0e0538b..1d6566e 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -15,3 +15,5 @@ Temporal is intentionally **not** the canonical owner of future obligations. If/ The initial stack definition is `deploy/docker-compose.prod.yml`. Portainer may deploy it from Git after the repository has been pushed. Credentials/API deployment automation is deliberately not embedded. Use `PORTAINER_API_KEY` from an external secret source or deploy through Portainer UI/API after authenticating. + +The single Creator login is configured with external `CREATOR_USERNAME` and `CREATOR_PASSWORD` values. They issue the same bounded Creator session and never select Actor, Principal, Scope or Authority. Keep `CREATOR_BOOTSTRAP_TOKEN` as a separate recovery/automation credential; none of these values belongs in Git, compose defaults for production, image layers or logs. diff --git a/modules/identity/domain/session.go b/modules/identity/domain/session.go index a2f01dc..77ee8f3 100644 --- a/modules/identity/domain/session.go +++ b/modules/identity/domain/session.go @@ -47,6 +47,16 @@ func ValidateCreatorSession(secret []byte, token string, now time.Time) (identit func BootstrapMatches(expected, supplied string) bool { return expected != "" && supplied != "" && subtle.ConstantTimeCompare([]byte(expected), []byte(supplied)) == 1 } + +func CreatorCredentialsMatch(expectedUsername, expectedPassword string, supplied identity.CreatorCredentials) bool { + if expectedUsername == "" || expectedPassword == "" || supplied.Username == "" || supplied.Password == "" { + return false + } + usernameMatches := subtle.ConstantTimeCompare([]byte(expectedUsername), []byte(supplied.Username)) + passwordMatches := subtle.ConstantTimeCompare([]byte(expectedPassword), []byte(supplied.Password)) + return usernameMatches&passwordMatches == 1 +} + func signature(secret []byte, payload string) string { mac := hmac.New(sha256.New, secret) mac.Write([]byte(payload)) diff --git a/tests/creator_auth/session_test.go b/tests/creator_auth/session_test.go index 70a2815..75cd615 100644 --- a/tests/creator_auth/session_test.go +++ b/tests/creator_auth/session_test.go @@ -2,6 +2,7 @@ package creator_auth_test import ( "encoding/json" + "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/modules/identity/domain" "strings" "testing" @@ -32,6 +33,23 @@ func TestCreatorSessionIsTimeBoundAndDoesNotCarryAuthority(t *testing.T) { } } +func TestCreatorUsernameAndPasswordMustBothMatch(t *testing.T) { + if !domain.CreatorCredentialsMatch("m", "password", identity.CreatorCredentials{Username: "m", Password: "password"}) { + t.Fatal("matching Creator credentials rejected") + } + for _, supplied := range []identity.CreatorCredentials{ + {}, + {Username: "m", Password: "wrong"}, + {Username: "wrong", Password: "password"}, + {Username: "m"}, + {Password: "password"}, + } { + if domain.CreatorCredentialsMatch("m", "password", supplied) { + t.Fatalf("invalid Creator credentials accepted: username=%q", supplied.Username) + } + } +} + func TestCreatorBootstrapCredentialMustMatch(t *testing.T) { if !domain.BootstrapMatches("expected", "expected") { t.Fatal("matching bootstrap credential rejected") diff --git a/tests/creator_web/verify-remote.ps1 b/tests/creator_web/verify-remote.ps1 index 3ba048c..92da63d 100644 --- a/tests/creator_web/verify-remote.ps1 +++ b/tests/creator_web/verify-remote.ps1 @@ -52,11 +52,21 @@ $invalid = Invoke-WebRequest -Uri "$BaseURL/api/session/current" -Headers @{Auth if ($invalid.StatusCode -ne 401) { throw "Invalid Creator session was not denied" } $environment = docker --context $DockerContext inspect --format '{{range .Config.Env}}{{println .}}{{end}}' $coreContainer +$usernameEntry = $environment | Where-Object { $_ -like "CREATOR_USERNAME=*" } | Select-Object -First 1 +$passwordEntry = $environment | Where-Object { $_ -like "CREATOR_PASSWORD=*" } | Select-Object -First 1 +if (-not $usernameEntry -or -not $passwordEntry) { throw "Creator username/password is not configured" } +$creatorUsername = $usernameEntry.Substring("CREATOR_USERNAME=".Length) +$creatorPassword = $passwordEntry.Substring("CREATOR_PASSWORD=".Length) +$invalidCredentials = Invoke-WebRequest -Uri "$BaseURL/api/session" -Method Post -ContentType "application/json" -Body (@{username=$creatorUsername;password="invalid"} | ConvertTo-Json -Compress) -SkipHttpErrorCheck +if ($invalidCredentials.StatusCode -ne 401) { throw "Invalid Creator credentials were not denied" } +$issued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -ContentType "application/json" -Body (@{username=$creatorUsername;password=$creatorPassword} | ConvertTo-Json -Compress) +if (-not $issued.Token) { throw "Creator username/password session was not issued through the gateway" } + $bootstrapEntry = $environment | Where-Object { $_ -like "CREATOR_BOOTSTRAP_TOKEN=*" } | Select-Object -First 1 if (-not $bootstrapEntry) { throw "Creator bootstrap token is not configured" } $bootstrapToken = $bootstrapEntry.Substring("CREATOR_BOOTSTRAP_TOKEN=".Length) -$issued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -Headers @{Authorization="Bearer $bootstrapToken"} -if (-not $issued.Token) { throw "Creator session was not issued through the gateway" } +$recoveryIssued = Invoke-RestMethod -Uri "$BaseURL/api/session" -Method Post -Headers @{Authorization="Bearer $bootstrapToken"} +if (-not $recoveryIssued.Token) { throw "Creator recovery session was not issued through the gateway" } $current = Invoke-RestMethod -Uri "$BaseURL/api/session/current" -Method Get -Headers @{Authorization="Bearer $($issued.Token)"} if ($current.ActorID -ne "domovoy-creator-actor") { throw "Creator identity was not preserved through the gateway" } @@ -96,4 +106,4 @@ if ($staleAction.StatusCode -ne 409) { throw "Stale Creator action was not rejec $arbitraryAction = Invoke-WebRequest -Uri "$BaseURL/api/actions/arbitrary" -Method Post -ContentType "application/json" -Headers @{Authorization="Bearer $($issued.Token)"} -Body '{}' -SkipHttpErrorCheck if ($arbitraryAction.StatusCode -ne 404) { throw "Arbitrary action route was exposed" } -Write-Output "DEV-000032 Creator satisfaction console gateway: PASS" +Write-Output "DEV-000033 Creator username/password and console gateway: PASS"