You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
The Dex UpdatePassword request construction risks unintentionally clearing credentials/fields by always sending empty defaults, and should be corrected before merge.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR adds support for provisioning and managing users in Dex (as an alternative to Keycloak), including hashing plaintext passwords for Dex’s password store and mapping Otomi user roles/teams to Dex group strings.
Changes:
Introduces AUTH_PROVIDER and DEX_GRPC_ADDRESS env configuration to switch between Keycloak and Dex provisioning paths.
Adds a Dex gRPC client, group mapping helpers, and bcrypt-based password hashing utilities.
Updates user CRUD flows, JWT group parsing, and OpenAPI requirements to support Dex-backed users and optional first/last names.
File summaries
File
Description
src/validators.ts
Adds AUTH_PROVIDER and DEX_GRPC_ADDRESS validators.
src/utils/userUtils.ts
Adds Dex group derivation + Dex Password→User mapping.
src/utils/userUtils.test.ts
Tests for Dex group derivation and mapping helpers.
src/utils/passwordUtils.ts
Adds bcrypt hashing helper for provisioning Dex password records.
src/utils/passwordUtils.test.ts
Tests for bcrypt hashing behavior (verify + salted).
src/proto/dex/api.proto
Adds Dex admin API proto for TS client generation.
Optional integration test for real Dex gRPC endpoint.
src/otomi-stack.ts
Adds Dex-backed implementations for user CRUD + team membership edits.
src/otomi-stack.test.ts
Adds tests covering Dex provisioning and Dex-mode user operations.
src/openapi/user.yaml
Makes firstName/lastName optional in the User schema.
src/middleware/jwt.ts
Ignores Dex no-groups sentinel when mapping JWT groups→roles/teams.
src/middleware/jwt.test.ts
Adds coverage for sentinel behavior in JWT claim mapping.
package.json
Adds dependencies and build/postinstall steps to generate Dex client code.
package-lock.json
Locks new deps for Dex gRPC client generation and bcrypt hashing.
eslint.config.mjs
Excludes src/generated/* from linting.
.gitignore
Ignores generated Dex client output under src/generated/.
Review details
Suppressed comments (1)
src/otomi-stack.ts:1547
When userData.id is missing, the error message currently becomes "User undefined not found", which is misleading and makes debugging harder. Return an explicit “id is required” message for this validation failure.
if (!userData.id) {
throw new NotExistError(`User ${userData.id} not found`)
}
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
package.json currently includes a duplicate JSON key (breaking/ambiguous metadata) and there are remaining production-readiness concerns around pulling gRPC client deps into middleware plus plaintext gRPC credentials.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (6)
Previously missed (1) — in code that hasn't changed since the last review.
src/utils/userUtils.test.ts:2
This test hard-codes the Dex "no groups" sentinel string. Importing and using DEX_NO_GROUPS_SENTINEL avoids drift if the sentinel value ever changes.
This issue also appears on line 70 of the same file.
package.json:128
package.json contains a duplicate "name" property, which makes the JSON ambiguous and can confuse tooling (only the last key wins). Remove the duplicate entry.
The test scripts rely on src/generated/dex being present (generated by postinstall), but running git clean -fdx or deleting ignored files can remove it and npm test won't regenerate it. Consider generating the Dex client as part of the test scripts too, similar to the build script.
"postinstall": "npm run build:models && npm run gen:dex-client",
src/utils/userUtils.test.ts:72
Use the shared DEX_NO_GROUPS_SENTINEL constant instead of the literal 'no_groups' so the test stays consistent with production behavior.
it('strips the no-groups sentinel and treats it as no groups at all', () => {
expect(dexPasswordToUser(password({ groups: ['__no_groups__'] }))).toMatchObject({
isPlatformAdmin: false,
src/middleware/jwt.ts:4
jwt middleware imports DEX_NO_GROUPS_SENTINEL from src/clients/dexClient, which also pulls in @grpc/grpc-js and env parsing on every server startup even when AUTH_PROVIDER=keycloak. Consider moving the sentinel constant into a lightweight constants module (imported by both dexClient and jwt) to avoid unnecessary dependencies in the hot-path middleware.
import { DEX_NO_GROUPS_SENTINEL } from 'src/clients/dexClient'
src/clients/dexClient.ts:42
DexClient is created with ChannelCredentials.createInsecure(), which sends credentials and password hashes over plaintext. If this will be used outside strictly local/dev networks, please wire TLS (e.g., createSsl() with configurable CA/hostname) or gate insecure mode behind an explicit environment flag.
// TODO(#3536): createInsecure() is a known temporary gap pending TLS wiring in apl-core.
client = new DexClient(env.DEX_GRPC_ADDRESS, ChannelCredentials.createInsecure())
}
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
There are correctness/operational blockers in the current diff (e.g., Dex-mode editUser can return an email change that cannot actually be persisted, and package.json has a duplicate key) that should be resolved before approval.
Review details
Suppressed comments (4)
Previously missed (1) — in code that hasn't changed since the last review.
src/otomi-stack.ts:1395
In Dex mode, editUser can accept an updated email but the Dex UpdatePassword API uses the existing email as an immutable lookup key, so the change will not persist. The method currently returns a User object with the new email anyway, which makes the API response inconsistent with subsequent reads. Also, password updates in Dex mode should enforce the same minimum length as createUser to avoid accepting weak passwords.
package.json:129
package.json contains the "name" property twice; duplicate JSON keys can lead to unpredictable behavior across tooling (some parsers reject duplicates). Remove the duplicate entry so the package metadata is unambiguous.
Dex gRPC is currently instantiated with ChannelCredentials.createInsecure(), which means traffic (including password hashes and group membership updates) is sent without transport security. If Dex is reachable beyond a strictly trusted network boundary, this can enable interception or tampering. Consider wiring TLS/mTLS (or explicitly limiting the address to a local/cluster-internal endpoint) before enabling AUTH_PROVIDER=dex in production.
throw new DexProvisionError('DEX_GRPC_ADDRESS must be set when AUTH_PROVIDER=dex')
}
if (!client) {
// TODO(#3536): createInsecure() is a known temporary gap pending TLS wiring in apl-core.
client = new DexClient(env.DEX_GRPC_ADDRESS, ChannelCredentials.createInsecure())
package.json:153
The Dex client types are generated into src/generated/dex, and postinstall/build now run gen:dex-client, but the test scripts still only run build:models. If installs are performed with --ignore-scripts (skipping postinstall) or src/generated is cleaned, npm test / npm run test:pattern will fail to compile. Consider running gen:dex-client as part of the test scripts too.
"lint-staged": "lint-staged",
"postinstall": "npm run build:models && npm run gen:dex-client",
"pre-release:client": "npm version prerelease --preid rc --no-commit-hooks --no-git-tag-version && bin/release-client.sh",
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
It introduces a new authentication/user-provisioning backend (Dex) plus password-handling and gRPC integration that should receive final human verification.
Review details
Suppressed comments (3)
Previously missed (1) — in code that hasn't changed since the last review.
src/proto/dex/api.proto:243
Typo in comment: "disovery" should be "discovery".
src/otomi-stack.ts:1524
This comment suggests the Dex team update batch is all-or-nothing ("each update either lands in Dex or the whole request rejects"), but the implementation applies updates sequentially and can leave partial updates persisted if a later call fails (see the for-loop below and the corresponding test case that expects two calls then a rejection). Please update the comment (or implement rollback/transaction semantics) to reflect the actual behavior.
// Dex has no Git counterpart to keep in sync, so there's no two-pass ordering to worry about
// here — each update either lands in Dex or the whole request rejects.
private async editDexTeamUsers(
src/otomi-stack.ts:1544
When userData.id is missing, this throws NotExistError with message User undefined not found, which is misleading (the input is invalid, not a missing user record). Consider returning a 400 with a clear message that the id is required.
if (!userData.id) {
throw new NotExistError(`User ${userData.id} not found`)
}
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
There are a few misleading comments/docs and an unclear error message in newly added Dex-mode code paths that should be corrected to match actual behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (2)
src/otomi-stack.ts:1524
The comment implies the batch is all-or-nothing, but updates are applied sequentially and a failure partway through can leave earlier users updated in Dex. This is misleading for future maintainers and incident triage.
// Dex has no Git counterpart to keep in sync, so there's no two-pass ordering to worry about
// here — each update either lands in Dex or the whole request rejects.
private async editDexTeamUsers(
src/otomi-stack.ts:1544
When userData.id is missing, the error message interpolates to "User undefined not found", which is confusing and makes debugging bad requests harder.
if (!userData.id) {
throw new NotExistError(`User ${userData.id} not found`)
}
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
There is a confirmed JWT group-to-team mapping bug (team-admin can be misinterpreted as team admin) plus a couple of test-side issues that should be addressed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (1)
src/otomi-stack.ts:1544
When userData.id is missing, the error message becomes User undefined not found, which is misleading (this is an invalid request rather than a missing user). Use a clearer message for the missing-id case.
if (!userData.id) {
throw new NotExistError(`User ${userData.id} not found`)
}
The reason will be displayed to describe this comment to others. Learn more.
🟡 Changes recommended
Critical correctness and security issues remain in Dex error handling, metadata persistence, team updates, and bcrypt input handling.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
Suppressed comments (7)
src/clients/dexClient.integration.test.ts:12
This fixture is not a complete bcrypt hash: it is 56 characters, whereas the bcrypt encoding stored by Dex is 60 characters. A real Dex instance may reject it or persist a password that can never authenticate, so the integration test should use a known valid bcrypt hash.
callWithRetry retries every thrown error, but the callbacks below intentionally throw DexProvisionError for non-transient alreadyExists and notFound responses. That produces four identical RPCs for deterministic failures and can make a successful create whose response is lost look like a failed duplicate after retry; restrict retries to transient gRPC status codes so application-level responses are not retried.
listDexPasswords is the read path for deduplication, getUser, getAllUsers, and edits, but dexClient.test.ts never mocks or calls listPasswords; the current mock client does not define it. Add success and RPC-error tests so regressions in the request/response mapping and retry behavior cannot pass unnoticed.
export async function listDexPasswords(): Promise<Password[]> {
const dex = getDexClient()
return callWithRetry(
() =>
new Promise<Password[]>((resolve, reject) => {
src/openapi/user.yaml:118
editDexUser also accepts data.initialPassword and hashes it at lines 1406-1410 to reset a Dex password, so saying this may be set only "on create" is inconsistent with the edit behavior introduced here. Update the description to document password resets on edit and that the field is ignored for other providers.
description: The initial password of the user. With Dex as issuer, an admin may set this on create; otherwise one is generated.
src/otomi-stack.test.ts:833
When originalAuthProvider is undefined, assigning it back with process.env.AUTH_PROVIDER = originalAuthProvider stores the string "undefined" rather than removing the variable in Node. That can leak an invalid provider value into later tests and make subsequent cleanEnv calls fail; delete the property when the original value was absent.
editDexTeamUsers does not validate the requested team IDs before writing groups, unlike createUser and editDexUser. A platform admin can therefore store team-<nonexistent> in Dex; dexPasswordToUser reports that team while JWT processing ignores it as unknown, leaving the API and authorization views inconsistent. Validate updatedUser before the RPC.
Because DEX_GRPC_ADDRESS is optional in cleanEnv and this check runs only inside getDexClient, a deployment with AUTH_PROVIDER=dex but no address starts successfully and only fails later when a user operation is attempted. Since the validator documents this value as required for Dex, validate the combination during application/stack initialization instead of deferring the configuration error to requests.
export const DEX_GRPC_ADDRESS = str({
desc: 'host:port of the Dex gRPC API. Required when AUTH_PROVIDER=dex.',
example: 'dex-grpc.dex.svc:5557',
devDefault: 'localhost:5557',
default: undefined,
Add direct coverage for deleteDexUserIdentity failures
src/clients/dexClient.ts:164
deleteDexUserIdentity is part of the new account-purge path but is not exercised by dexClient.test.ts; the stack tests mock this export and therefore cannot catch a malformed request or callback error-handling regression. Add a direct wrapper test for the { userId, connectorId } request and an RPC failure.
Unknown IDs incorrectly return success instead of 404
src/otomi-stack.ts:1466
When both lookups are empty this returns 200 for any unknown ID, whereas the existing Git-backed path calls requireUserSecretData and returns a 404 for a user that never existed. This also makes a genuinely invalid ID indistinguishable from a previously partially purged account; preserve the not-found contract or introduce an explicit deletion/tombstone mechanism rather than treating every empty lookup as an idempotent retry.
The reason will be displayed to describe this comment to others. Learn more.
Copilot review overview
🔵 Needs a closer look
Resolve the CI fork authentication issue, synchronize package metadata in the lockfile, and align the OpenAPI password constraint with backend behavior.
The package name was changed to @linode/apl-api, but package-lock.json still declares @redkubes/otomi-api for both its root metadata entries (lines 2 and 8). Please regenerate or update the lockfile so npm ci and package tooling use the same package identity as package.json.
Add tests for identity filtering and RPC errors
src/clients/dexClient.ts:155
This new identity-listing path is security-critical for Dex deletion, but dexClient.test.ts does not exercise its filtering or RPC-error behavior (the stack tests mock this function entirely). Add client-level tests for returning only the requested user's identities and propagating a failed listUserIdentities RPC before relying on it for account purges.
This workflow runs on pull_request, including forked PRs, but secrets.BOT_TOKEN is not exposed to fork workflows. The subsequent npm install therefore cannot authenticate for the new @linode/dex-client-grpc dependency, so coverage will fail for those PRs; use an authentication strategy available to fork runs or skip this job for untrusted forks.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.