mirror of
https://github.com/photoprism/photoprism.git
synced 2026-07-17 16:49:04 +00:00
Users: Strengthen Add Account and built-in super-admin form gating
This commit is contained in:
parent
fd02897278
commit
3e5ea71fb6
2 changed files with 43 additions and 20 deletions
|
|
@ -224,6 +224,12 @@ export class User extends RestModel {
|
|||
return this.Role === "cluster_admin";
|
||||
}
|
||||
|
||||
// isInitialAdmin returns true for the built-in super admin created at setup (ID 1).
|
||||
// It is the lockout-recovery operator, so the admin UI locks its privileged controls.
|
||||
isInitialAdmin() {
|
||||
return this.ID === 1;
|
||||
}
|
||||
|
||||
// isCurrentUser returns true when this account belongs to the signed-in user.
|
||||
// The admin UI uses it to lock fields that would otherwise let an operator
|
||||
// lock themselves out (own role, web login, and authentication provider); the
|
||||
|
|
@ -233,10 +239,6 @@ export class User extends RestModel {
|
|||
return !!(current && current.UID && this.UID && current.UID === this.UID);
|
||||
}
|
||||
|
||||
requiresPassword() {
|
||||
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() {
|
||||
|
|
@ -278,14 +280,20 @@ export class User extends RestModel {
|
|||
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 reports whether the new account has enough to authenticate.
|
||||
// Gates the Add button independently of lazy field validation, so it must hold on its
|
||||
// own: it requires the credential each provider mandates rather than trusting form state.
|
||||
hasLoginCredential() {
|
||||
if (this.AuthProvider !== "default" || !this.showsAuthIdField()) {
|
||||
return true;
|
||||
if (this.passwordIsRequired()) {
|
||||
return !!this.Password;
|
||||
}
|
||||
return !!this.Password || $config.ldapEnabled() || !!this.AuthID;
|
||||
if (this.authIdIsRequired()) {
|
||||
return !!this.AuthID;
|
||||
}
|
||||
if (this.AuthProvider === "default" && this.showsAuthIdField()) {
|
||||
return !!this.Password || !!this.AuthID;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// canEnableLogin reports whether web/API login can be enabled for this account.
|
||||
|
|
|
|||
|
|
@ -456,6 +456,11 @@ describe("model/user", () => {
|
|||
expect(new User({ ID: 2, Name: "b", Role: "cluster_admin" }).isClusterAdmin()).toBe(true);
|
||||
expect(new User({ ID: 1, Name: "a", Role: "admin" }).isClusterAdmin()).toBe(false);
|
||||
});
|
||||
it("isInitialAdmin is true only for the built-in super admin (ID 1)", () => {
|
||||
expect(new User({ ID: 1, Name: "admin", Role: "admin", SuperAdmin: true }).isInitialAdmin()).toBe(true);
|
||||
expect(new User({ ID: 2, Name: "sven", Role: "admin", SuperAdmin: true }).isInitialAdmin()).toBe(false);
|
||||
expect(new User({ ID: 0, Name: "" }).isInitialAdmin()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Account-dialog auth field gating shared by the Pro and Portal "Add Account"
|
||||
|
|
@ -516,26 +521,36 @@ describe("model/user", () => {
|
|||
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);
|
||||
it("requires a local password when one is mandatory, independent of form timing", () => {
|
||||
// "local" always needs a password, so the Add button must stay disabled until it is set.
|
||||
expect(new User({ AuthProvider: "local" }).hasLoginCredential()).toBe(false);
|
||||
expect(new User({ AuthProvider: "local", Password: "secret123" }).hasLoginCredential()).toBe(true);
|
||||
// "default" without external auth behaves like a local account.
|
||||
expect(new User({ AuthProvider: "default" }).hasLoginCredential()).toBe(false);
|
||||
expect(new User({ AuthProvider: "default", Password: "secret123" }).hasLoginCredential()).toBe(true);
|
||||
});
|
||||
it("requires a password or Subject ID for default with OIDC configured", () => {
|
||||
setExt({ oidc: { enabled: true } });
|
||||
// default + OIDC only, neither password nor Subject ID → inert, blocked.
|
||||
// default + OIDC, 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", () => {
|
||||
it("requires the AuthID (DN) for default authenticating against LDAP", () => {
|
||||
setExt({ ldap: { enabled: true } });
|
||||
// default + LDAP: the username resolves the account via the directory.
|
||||
expect(new User({ AuthProvider: "default" }).hasLoginCredential()).toBe(true);
|
||||
// default + LDAP: a globally enabled directory is not enough; a DN (or password) is required.
|
||||
expect(new User({ AuthProvider: "default" }).hasLoginCredential()).toBe(false);
|
||||
expect(new User({ AuthProvider: "default", AuthID: "cn=jane,dc=example,dc=com" }).hasLoginCredential()).toBe(true);
|
||||
expect(new User({ AuthProvider: "default", Password: "secret123" }).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", () => {
|
||||
it("requires the Subject ID for an explicit OIDC account, independent of form timing", () => {
|
||||
expect(new User({ AuthProvider: "oidc" }).hasLoginCredential()).toBe(false);
|
||||
expect(new User({ AuthProvider: "oidc", AuthID: "sub-123" }).hasLoginCredential()).toBe(true);
|
||||
});
|
||||
it("defers to the per-field rules for the none provider", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue