From 4d6bd46248aa0286014ecce17fef0dda71cc1794 Mon Sep 17 00:00:00 2001 From: Greg Kostin Date: Wed, 29 Apr 2026 14:16:50 -0400 Subject: [PATCH 01/11] feat: config-driven showPasswordLogin exclusive toggle (DEEPBLUE-466) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a boolean config flag `auth.showPasswordLogin` that controls which login method type is shown in the DSpace Angular login UI. The flag is an exclusive toggle — exactly one type renders at a time, never both, never none. false (default): OIDC button shown, password form hidden -> production and workshop are unchanged true: password form shown, OIDC button hidden -> for environments where DSpace OIDC is disabled (demo) Background ---------- The demo environment uses oauth2-proxy at the gate for U-M WebLogin, and DSpace's internal OIDC is intentionally disabled so testers can switch personas via shared EPerson accounts. The existing UM customisation (*ngIf="authMethod.authMethodType !== 'password'") removed the password form entirely, leaving a blank login page once DSpace OIDC was disabled. Changes ------- src/config/auth-config.interfaces.ts Add showPasswordLogin?: boolean to AuthConfig interface. src/config/default-app-config.ts Default showPasswordLogin: false in the auth block. src/app/shared/log-in/log-in.component.ts Inject APP_CONFIG; read flag into public showPasswordLogin property. src/app/shared/log-in/log-in.component.html Replace hard-coded *ngIf="... !== 'password'" with exclusive toggle: *ngIf="showPasswordLogin === (authMethod.authMethodType === 'password')" src/app/shared/log-in/log-in.component.spec.ts Provide APP_CONFIG token; update assertion toBe(2) -> toBe(1) (only non-password method renders with default showPasswordLogin: false). config/config.example.yml Add commented-out showPasswordLogin entry for operator reference. Configuration ------------- Enable for demo via Kubernetes frontend ConfigMap env-var: DSPACE_AUTH_SHOWPASSWORDLOGIN=true The DSpace Angular config system will convert the string 'true' to a proper boolean via getBooleanFromString (confirmed). Production and workshop must NOT set this variable — omitting it leaves the default false. Testing ------- Unit tests: 2 specs pass (log-in.component.spec.ts). See PLAN_DSPACE_ANGULAR_PASSWORD_LOGIN.md for full rationale, config flow deep-dive, on-host debugging commands, and demo verification steps. --- AGENTS.md | 177 +++++++++++ DONE.md | 86 ++++++ PLAN_DSPACE_ANGULAR_PASSWORD_LOGIN.md | 292 ++++++++++++++++++ TODO.md | 16 + config/config.example.yml | 9 +- src/app/shared/log-in/log-in.component.html | 6 +- .../shared/log-in/log-in.component.spec.ts | 7 +- src/app/shared/log-in/log-in.component.ts | 13 +- src/config/auth-config.interfaces.ts | 5 + src/config/default-app-config.ts | 3 +- 10 files changed, 605 insertions(+), 9 deletions(-) create mode 100644 AGENTS.md create mode 100644 DONE.md create mode 100644 PLAN_DSPACE_ANGULAR_PASSWORD_LOGIN.md create mode 100644 TODO.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..8ba2f24f24e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,177 @@ +# AGENTS.md — Coding Agent Guidelines for `show-password-login` + +## Purpose + +This file gives a coding agent everything it needs to continue (or complete) the +`show-password-login` feature branch in `mlibrary/dspace-angular` without needing +to ask the human for context. + +--- + +## Repository & Branch + +| Item | Value | +|---|---| +| Repo | `mlibrary/dspace-angular` | +| Active branch | `show-password-login` | +| Base branch | `umich` / `issue-working` | +| Framework | Angular 14 / DSpace 7.6 | +| Language | TypeScript + HTML templates | + +--- + +## The Problem Being Solved + +A UM-specific customisation in `src/app/shared/log-in/log-in.component.html` hard-codes +the password auth method out of the login UI: + +```html +*ngIf="authMethod.authMethodType !== 'password'" +``` + +On the **demo** environment, DSpace OIDC is disabled, making `password` the only advertised +auth method — leaving a completely blank login form. No one can log in to demo. + +Full context: read `PLAN_DSPACE_ANGULAR_PASSWORD_LOGIN.md`. + +--- + +## The Chosen Solution + +A **config-driven exclusive toggle**: a new `showPasswordLogin` boolean in `AuthConfig`. + +- `false` (default) → show OIDC/non-password methods only. Production & workshop unchanged. +- `true` (demo only) → show password form only, hide OIDC. + +Template condition (exclusive — never both, never none): +```html +*ngIf="showPasswordLogin === (authMethod.authMethodType === 'password')" +``` + +--- + +## Work Tracking Files + +| File | Purpose | +|---|---| +| `TODO.md` | Remaining tasks in priority order — work from top to bottom | +| `DONE.md` | Completed tasks — move items here when a task is committed | +| `PLAN_DSPACE_ANGULAR_PASSWORD_LOGIN.md` | Full design rationale and exact code snippets | + +**Always check `TODO.md` first** to see what still needs doing. +**After completing and committing each task**, move it from `TODO.md` to `DONE.md`. + +--- + +## Key Files to Know + +| File | Role | +|---|---| +| `src/config/auth-config.interfaces.ts` | `AuthConfig` interface — add `showPasswordLogin?: boolean` | +| `src/config/default-app-config.ts` | `DefaultAppConfig` class — set `showPasswordLogin: false` in `auth` block | +| `src/app/shared/log-in/log-in.component.ts` | Inject `APP_CONFIG`, read flag, expose as `showPasswordLogin` property | +| `src/app/shared/log-in/log-in.component.html` | Replace `*ngIf` with exclusive-toggle condition | +| `src/app/shared/log-in/log-in.component.spec.ts` | Unit test — needs `APP_CONFIG` provided and assertion updated to `toBe(1)` | +| `src/app/shared/testing/auth-service.stub.ts` | `authMethodsMock` = `[password, shibboleth]` — used by the spec | +| `src/environments/environment.test.ts` | Full `auth:` block used by specs; does NOT have `showPasswordLogin` (omitting it is fine — `undefined ?? false` → `false`) | +| `config/config.example.yml` | Operator reference — add commented-out `showPasswordLogin` entry in the `auth:` block | + +--- + +## How `APP_CONFIG` Is Used in This Codebase + +`APP_CONFIG` is an Angular `InjectionToken` defined in +`src/config/app-config.interface.ts`. Inject it into a component constructor like this: + +```typescript +import { AppConfig, APP_CONFIG } from '../../../config/app-config.interface'; +// Path from src/app/shared/log-in/ → three levels up → src/config/ + +constructor( + // ...existing params... + @Inject(APP_CONFIG) private appConfig: AppConfig +) {} +``` + +Existing examples to reference: +- `src/app/community-list-page/community-list-service.ts` +- `src/app/item-page/edit-item-page/virtual-metadata/virtual-metadata.component.ts` + +In test files, provide it as: +```typescript +import { APP_CONFIG } from '../../../config/app-config.interface'; +import { environment } from '../../../environments/environment'; +// ... +{ provide: APP_CONFIG, useValue: environment } +``` + +`environment` (resolves to `environment.test.ts` during tests) has a full `auth:` block +but no `showPasswordLogin` key. That is fine — the component reads it as +`appConfig.auth?.showPasswordLogin ?? false`, so `undefined ?? false` → `false`. +No change to `environment.test.ts` is required. + +--- + +## Spec File Notes + +`src/app/shared/log-in/log-in.component.spec.ts` currently has: + +```typescript +it('should render a log-in container component for each auth method available', () => { + const loginContainers = fixture.debugElement.queryAll(By.css('ds-log-in-container')); + expect(loginContainers.length).toBe(2); // ← must change to toBe(1) +}); +``` + +`authMethodsMock` contains `[password, shibboleth]`. With `showPasswordLogin: false` (default), +only `shibboleth` renders → `toBe(1)`. The `APP_CONFIG` token must also be added to +`providers` in `TestBed.configureTestingModule` or the component will fail to construct. + +--- + +## `config/config.example.yml` — Operator Documentation + +The `auth:` block in `config/config.example.yml` (around line 91–102) must have a +commented-out `showPasswordLogin` entry added so operators know the option exists: + +```yaml +# When true, show the username/password login form and hide OIDC (exclusive toggle). +# Set to true only on environments where DSpace OIDC is disabled (e.g. demo). +# Default: false (OIDC button shown, password form hidden — production/workshop behaviour). +# Can also be set via environment variable: DSPACE_AUTH_SHOWPASSWORDLOGIN=true +# showPasswordLogin: false +``` + +--- + +## Config Override for Demo (Out-of-Band) + +The demo Kubernetes frontend Deployment/ConfigMap (in `mlibrary/deepblue-documents-kube`, +**not** this repo) must set: + +``` +DSPACE_AUTH_SHOWPASSWORDLOGIN=true +``` + +This task is tracked in `TODO.md` item 9. It is performed separately from this repo's code changes. + +--- + +## Coding Guidelines + +1. **Read `TODO.md` first.** Work tasks in the listed order (they have dependencies). +2. **After completing and committing each task**, update `TODO.md` (mark done / remove) and + add the task to `DONE.md`. +3. **Do not change any file not listed in `TODO.md`** unless fixing a compile/lint error + directly caused by your changes. +4. **After editing each TypeScript file**, run `get_errors` on that file and fix any issues + before moving on. +5. **Run the spec** after updating the spec file: + `npx ng test --include='**/log-in/log-in.component.spec.ts' --watch=false` +6. **Commit message convention:** + `feat: config-driven showPasswordLogin exclusive toggle (DEEPBLUE-466)` +7. The `config/config.yml` in this repo is **not** the demo config — do not modify it. + The demo override happens via environment variable in the Kubernetes repo. +8. Do not remove the ` +*ngIf="authMethod.authMethodType !== 'password'" + + +*ngIf="showPasswordLogin === (authMethod.authMethodType === 'password')" +``` + +#### `src/app/shared/log-in/log-in.component.spec.ts` +- Added `APP_CONFIG` and `environment` imports; provided `APP_CONFIG` in `TestBed`. +- Updated assertion from `toBe(2)` to `toBe(1)` (with `showPasswordLogin: false`, only the + shibboleth method from `authMethodsMock` renders). + +#### `config/config.example.yml` +Added operator-facing documentation in the `auth:` block: +```yaml +# When true, show the username/password login form and hide OIDC (exclusive toggle). +# Set to true only on environments where DSpace OIDC is disabled (e.g. demo). +# Default: false (OIDC button shown, password form hidden — production/workshop behaviour). +# Can also be set via environment variable: DSPACE_AUTH_SHOWPASSWORDLOGIN=true +# showPasswordLogin: false +``` + +--- + +## How to Configure for Demo + +The `config/config.yml` in this repo is **not** the demo config. The demo override is applied +via environment variable in the frontend Kubernetes Deployment/ConfigMap +(in `mlibrary/deepblue-documents-kube`, **not** this repo): + +``` +DSPACE_AUTH_SHOWPASSWORDLOGIN=true +``` + +This follows the DSpace Angular environment-variable naming convention +(see `docs/Configuration.md`): replace `.` with `_`, uppercase, prefix with `DSPACE_`. + +Alternatively, the demo-mounted `config.yml` can include: +```yaml +auth: + showPasswordLogin: true +``` + +**Production and workshop** must NOT set this variable — omitting it leaves the default +`false`, preserving the current OIDC-only login behaviour. + +--- + +## Why This Change Is Safe + +- **Default is `false`** — every environment that doesn't set the flag is unchanged. +- **Production** — no config change → `showPasswordLogin: false` → OIDC button shown, password hidden. ✅ +- **Workshop** — same as production. ✅ +- **Demo** — `DSPACE_AUTH_SHOWPASSWORDLOGIN=true` → password form shown, OIDC button hidden. ✅ +- The `ds-log-in-container` component's `rendersAuthMethodType` decorator handles each auth + method type correctly; the only change is which methods pass the `*ngIf` gate. + +--- + +## How the Auth Methods List Is Populated + +`log-in.component.ts` subscribes to `getAuthenticationMethods` from the NgRx store, which +is populated from the DSpace REST API response at `/api/authn`. The backend returns available +methods as HAL links: + +- `_links.login` → password auth available (`authMethodType: 'password'`) +- `_links.oidcLogin` → OIDC auth available (`authMethodType: 'oidc'`) + +IP auth is explicitly filtered by `log-in.component.ts` before the template sees the list. + +**Demo backend returns** (DSpace OIDC disabled — only `login` link present): +```json +{ + "_links": { + "login": { "href": "https://backend.demo.deepblue-documents.lib.umich.edu/server/api/authn/login" } + } +} +``` +`authMethods` = `[{ authMethodType: 'password' }]`. With `showPasswordLogin: true`, the +password form renders. With the default `false` it would be filtered out (blank form). + +--- + +## Config System Deep-Dive — How the Env-Var Reaches the Component + +This section exists so that if the flag doesn't appear to work, an agent or operator can +trace exactly where the breakdown is. + +### The chain (server-side rendering path) + +1. **`server.ts` (startup):** + ```typescript + const appConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json')); + extendEnvironmentWithAppConfig(environment, appConfig); + // ... + { provide: APP_CONFIG, useValue: environment } + ``` + `buildAppConfig` reads `DefaultAppConfig`, merges `config/config.yml`, then calls + `overrideWithEnvironment(appConfig)` which walks every property and checks for a matching + `DSPACE_*` env-var. For `auth.showPasswordLogin`, it looks for `DSPACE_AUTH_SHOWPASSWORDLOGIN`. + +2. **`overrideWithEnvironment` (in `src/config/config.server.ts`) — key details:** + - Requires the property to **already exist** in the config object (it only walks existing + properties). Because `DefaultAppConfig` sets `showPasswordLogin: false`, this property + exists and will be found. ✅ + - Checks `isNotEmpty(innerConfig)` before processing. Verified: `isNotEmpty(false) === true`, + so a default of `false` does **not** cause the env-var to be silently ignored. ✅ + - Uses `getBooleanFromString(value)` for boolean properties: + `'true' === 'true' || 'true' === '1'` → actual boolean `true`. ✅ + The component reads a proper boolean, not the string `"true"`. + +3. **`extendEnvironmentWithAppConfig`** mutates the `environment` object in place via `deepmerge`, + so `environment.auth.showPasswordLogin` becomes `true`. + +4. **`APP_CONFIG` token** is provided as `useValue: environment` — the now-extended object. + `LogInComponent` injects it and reads `appConfig.auth?.showPasswordLogin ?? false` → `true`. + +5. **SSR → browser transfer:** `saveAppConfigForCSR()` writes `environment` (already extended) + to Angular's `TransferState`. The browser-side `BrowserInitService` reads it back. The same + `true` value reaches the browser without an additional network round-trip. + +### How to verify the env-var was applied at startup + +The server logs this line when an env-var override fires: +``` +Applying environment variable DSPACE_AUTH_SHOWPASSWORDLOGIN with value true +``` +Check the frontend pod logs immediately after startup. If this line is absent, the env-var +was not set in the environment, or was set with an unexpected key/value. + +### On-host config inspection shortcut + +At startup, `buildAppConfig` writes the final computed config to: +``` +dist/browser/assets/config.json +``` +This is a plain JSON file. After the pod is running, inspect it with: +```shell +kubectl -n demo exec deploy/frontend -- cat /app/dist/browser/assets/config.json | python3 -m json.tool | grep -A2 showPassword +``` +Expected output when correctly configured: +```json +"auth": { + ... + "showPasswordLogin": true +} +``` + +### YAML config alternative — deep-merge safety + +When using `auth.showPasswordLogin: true` in the mounted `config.yml` (rather than the env-var), +`mergeConfig` uses `deepmerge`. The `auth` block is deep-merged, so specifying only +`showPasswordLogin` will not clobber `auth.ui` or `auth.rest`. The full auth block does not +need to be re-specified. ✅ + +--- + +## Remaining Step — Task 9 (different repo) + +In `mlibrary/deepblue-documents-kube`, add to the **frontend** demo Deployment/ConfigMap: +``` +DSPACE_AUTH_SHOWPASSWORDLOGIN=true +``` + +File: `environments/deepblue-documents/demo/` (frontend ConfigMap, not the backend one). + +Confirm production and workshop frontend ConfigMaps do **not** set this variable. + +--- + +## Verification Steps (after demo deployment) + +1. Navigate to `https://demo.deepblue-documents.lib.umich.edu` — U-M WebLogin gate challenges. +2. After passing the gate, click **Log In** in the navbar dropdown. +3. A username/password form should now appear (no OIDC button). +4. Log in as `dbrrds@umich.edu` (demo DSpace admin — see `DSPACE_ADMIN.md` in kube repo for password). +5. Confirm login succeeds and user appears as logged in within the DSpace UI. +6. Log out, log in as `tildon@umich.edu` (password `BlueDemo2026t`) to confirm persona switching. + +For workshop/production: login dropdown still shows **only** the OIDC button. No password form. ✅ + +--- + +## Demo Test Accounts (as of 2026-04-29) + +| Email | Name | Password | +|---|---|---| +| `dbrrds@umich.edu` | Admin, Admin | Reset during DEEPBLUE-466 — see `DSPACE_ADMIN.md` | +| `tildon@umich.edu` | Smith, Duey | `BlueDemo2026t` (temp — should be changed) | +| `pacerda@umich.edu` | Cerda, Peter | Needs reset via TTY (may be OIDC-only account) | +| `blancoj@umich.edu` | blanco, jose | No password set | +| `user1@umich.edu` | Usser one | No password set | +| `user2@umich.edu` | User Two | No password set | +| `user3@umich.edu` | user three | No password set | + +Accounts without passwords can be set via: +```shell +kubectl config use-context deepblue-documents-workshop +kubectl -n demo exec deploy/backend -- \ + /dspace/bin/dspace user --modify --email --password +``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000000..4fc02419841 --- /dev/null +++ b/TODO.md @@ -0,0 +1,16 @@ +# TODO — show-password-login implementation + +Tasks are listed in dependency order. Complete them top-to-bottom. +Move each task to `DONE.md` when it is committed to the `show-password-login` branch. + +--- + +## 9. Note in Kubernetes repo (out-of-band — different repository) + +This task is tracked here for completeness; it is performed in `mlibrary/deepblue-documents-kube`, +not in this repo. + +- [ ] Add `DSPACE_AUTH_SHOWPASSWORDLOGIN=true` to the **frontend** demo Deployment/ConfigMap + in `environments/deepblue-documents/demo/`. +- [ ] Confirm production and workshop frontend ConfigMaps do **not** set this variable + (they default to `false`). diff --git a/config/config.example.yml b/config/config.example.yml index d8f8929e56d..bbccdcb63e2 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -75,7 +75,7 @@ cache: anonymousCache: # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. # As all pages are cached in server memory, increasing this value will increase memory needs. - # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. max: 0 # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached # copy is automatically refreshed on the next request. @@ -100,6 +100,11 @@ auth: # If the rest token expires in less than this amount of time, it will be refreshed automatically. # This is independent from the idle warning. timeLeftBeforeTokenRefresh: 120000 # 2 minutes + # When true, show the username/password login form and hide OIDC (exclusive toggle). + # Set to true only on environments where DSpace OIDC is disabled (e.g. demo). + # Default: false (OIDC button shown, password form hidden — production/workshop behaviour). + # Can also be set via environment variable: DSPACE_AUTH_SHOWPASSWORDLOGIN=true + # showPasswordLogin: false # Form settings form: @@ -379,7 +384,7 @@ vocabularies: vocabulary: 'srsc' enabled: true -# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. +# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. comcolSelectionSort: sortField: 'dc.title' sortDirection: 'ASC' diff --git a/src/app/shared/log-in/log-in.component.html b/src/app/shared/log-in/log-in.component.html index 718c4d7fbd9..b25f58646cd 100644 --- a/src/app/shared/log-in/log-in.component.html +++ b/src/app/shared/log-in/log-in.component.html @@ -1,9 +1,9 @@