Users: Centralize Add Account auth field gating in the user model

This commit is contained in:
Michael Mayer 2026-06-25 16:24:55 +00:00
parent 41fdb578b2
commit d2a2597ab6
2 changed files with 135 additions and 2 deletions

View file

@ -237,6 +237,57 @@ export class User extends RestModel {
return !this.AuthProvider || this.AuthProvider === "default" || this.AuthProvider === "local";
}
// showsPasswordField reports whether the local password input is shown.
// OIDC may keep a local password as a fallback; LDAP replaces it; "none" disables auth.
showsPasswordField() {
return ["default", "local", "oidc"].includes(this.AuthProvider);
}
// passwordIsRequired reports whether a local password must be set to create the account.
// "default" needs one unless an external identity can be supplied instead (OIDC/LDAP configured).
passwordIsRequired() {
if (this.AuthProvider === "local") {
return true;
}
if (this.AuthProvider === "default") {
return !this.showsAuthIdField();
}
return false;
}
// showsAuthIdField reports whether the external identity input (OIDC Subject ID or LDAP DN) is shown.
// "default" offers it only when OIDC or LDAP is configured, to pre-provision external accounts.
showsAuthIdField() {
const p = this.AuthProvider;
return p === "oidc" || p === "ldap" || (p === "default" && ($config.oidcEnabled() || $config.ldapEnabled()));
}
// authIdIsRequired reports whether the external identity must be set; only OIDC requires it up front.
authIdIsRequired() {
return this.AuthProvider === "oidc";
}
// authIdIsDn reports whether the external identity is an LDAP DN rather than an OIDC Subject ID.
authIdIsDn() {
const p = this.AuthProvider;
return p === "ldap" || (p === "default" && $config.ldapEnabled() && !$config.oidcEnabled());
}
// authIdFieldLabel returns the label for the external identity input.
authIdFieldLabel() {
return this.authIdIsDn() ? "Distinguished Name (DN)" : "Subject ID";
}
// hasLoginCredential reports whether enough is set for the new account to authenticate.
// Covers the gap per-field rules miss: "default" with external auth configured needs a
// password, an LDAP directory (username resolves it), or an OIDC Subject ID — else it is inert.
hasLoginCredential() {
if (this.AuthProvider !== "default" || !this.showsAuthIdField()) {
return true;
}
return !!this.Password || $config.ldapEnabled() || !!this.AuthID;
}
// canEnableLogin reports whether web/API login can be enabled for this account.
// System users, role-less accounts, visitors, and deactivated accounts (provider
// "none") cannot log in regardless of the toggle. LDAP and OIDC accounts can.

View file

@ -1,10 +1,10 @@
import { describe, it, expect } from "vitest";
import { describe, it, expect, afterEach } from "vitest";
import "../fixtures";
import User from "model/user";
import File from "model/file";
import Config from "common/config";
import StorageShim from "node-storage-shim";
import { $session } from "app/session";
import { $config, $session } from "app/session";
const defaultConfig = new Config(new StorageShim(), window.__CONFIG__);
@ -457,4 +457,86 @@ describe("model/user", () => {
expect(new User({ ID: 1, Name: "a", Role: "admin" }).isClusterAdmin()).toBe(false);
});
});
// Account-dialog auth field gating shared by the Pro and Portal "Add Account"
// dialogs. The external-auth branches read the global $config (ext.oidc/ext.ldap),
// so each case sets and restores it explicitly.
describe("auth field gating", () => {
const setExt = (ext) => {
$config.values.ext = ext;
};
afterEach(() => {
setExt(undefined);
});
it("shows the password field for default, local and oidc but not ldap or none", () => {
expect(new User({ AuthProvider: "default" }).showsPasswordField()).toBe(true);
expect(new User({ AuthProvider: "local" }).showsPasswordField()).toBe(true);
expect(new User({ AuthProvider: "oidc" }).showsPasswordField()).toBe(true);
expect(new User({ AuthProvider: "ldap" }).showsPasswordField()).toBe(false);
expect(new User({ AuthProvider: "none" }).showsPasswordField()).toBe(false);
});
it("requires a password for local, and for default without an external identity", () => {
expect(new User({ AuthProvider: "local" }).passwordIsRequired()).toBe(true);
// "default" with no OIDC/LDAP configured must require a password (regression:
// previously optional, so the ADD button enabled with only a username).
expect(new User({ AuthProvider: "default" }).passwordIsRequired()).toBe(true);
expect(new User({ AuthProvider: "oidc" }).passwordIsRequired()).toBe(false);
expect(new User({ AuthProvider: "ldap" }).passwordIsRequired()).toBe(false);
expect(new User({ AuthProvider: "none" }).passwordIsRequired()).toBe(false);
});
it("makes the password optional for default when external auth can be supplied", () => {
setExt({ oidc: { enabled: true } });
expect(new User({ AuthProvider: "default" }).passwordIsRequired()).toBe(false);
expect(new User({ AuthProvider: "default" }).showsAuthIdField()).toBe(true);
});
it("shows the auth-id field for oidc and ldap, and for default only when configured", () => {
expect(new User({ AuthProvider: "oidc" }).showsAuthIdField()).toBe(true);
expect(new User({ AuthProvider: "ldap" }).showsAuthIdField()).toBe(true);
expect(new User({ AuthProvider: "default" }).showsAuthIdField()).toBe(false);
setExt({ ldap: { enabled: true } });
expect(new User({ AuthProvider: "default" }).showsAuthIdField()).toBe(true);
expect(new User({ AuthProvider: "local" }).showsAuthIdField()).toBe(false);
expect(new User({ AuthProvider: "none" }).showsAuthIdField()).toBe(false);
});
it("requires the auth-id only for an explicit oidc account", () => {
expect(new User({ AuthProvider: "oidc" }).authIdIsRequired()).toBe(true);
setExt({ oidc: { enabled: true } });
expect(new User({ AuthProvider: "default" }).authIdIsRequired()).toBe(false);
expect(new User({ AuthProvider: "ldap" }).authIdIsRequired()).toBe(false);
});
it("treats the auth-id as a DN for ldap, and for default when only LDAP is configured", () => {
expect(new User({ AuthProvider: "ldap" }).authIdIsDn()).toBe(true);
expect(new User({ AuthProvider: "oidc" }).authIdIsDn()).toBe(false);
setExt({ ldap: { enabled: true } });
expect(new User({ AuthProvider: "default" }).authIdIsDn()).toBe(true);
setExt({ oidc: { enabled: true }, ldap: { enabled: true } });
expect(new User({ AuthProvider: "default" }).authIdIsDn()).toBe(false);
});
it("labels the auth-id field from authIdIsDn", () => {
expect(new User({ AuthProvider: "ldap" }).authIdFieldLabel()).toBe("Distinguished Name (DN)");
expect(new User({ AuthProvider: "oidc" }).authIdFieldLabel()).toBe("Subject ID");
});
it("requires at least one credential for default with OIDC-only configured", () => {
// No external auth: the per-field password rule already covers it.
expect(new User({ AuthProvider: "default" }).hasLoginCredential()).toBe(true);
setExt({ oidc: { enabled: true } });
// default + OIDC only, neither password nor Subject ID → inert, blocked.
expect(new User({ AuthProvider: "default" }).hasLoginCredential()).toBe(false);
expect(new User({ AuthProvider: "default", Password: "secret123" }).hasLoginCredential()).toBe(true);
expect(new User({ AuthProvider: "default", AuthID: "sub-123" }).hasLoginCredential()).toBe(true);
});
it("treats the username as sufficient when LDAP can resolve the account", () => {
setExt({ ldap: { enabled: true } });
// default + LDAP: the username resolves the account via the directory.
expect(new User({ AuthProvider: "default" }).hasLoginCredential()).toBe(true);
// explicit ldap: a DN is optional, so a username-only account is allowed.
expect(new User({ AuthProvider: "ldap" }).hasLoginCredential()).toBe(true);
});
it("defers to the per-field rules for non-default providers", () => {
setExt({ oidc: { enabled: true } });
expect(new User({ AuthProvider: "local" }).hasLoginCredential()).toBe(true);
expect(new User({ AuthProvider: "oidc" }).hasLoginCredential()).toBe(true);
expect(new User({ AuthProvider: "none" }).hasLoginCredential()).toBe(true);
});
});
});