mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +01:00
OIDC: Improve CLI commands and add AuthIssuer to users and sessions #782
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -8,7 +8,7 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2024-07-08 14:23+0000\n"
|
"POT-Creation-Date: 2024-07-09 06:08+0000\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
|
|||||||
@@ -49,9 +49,12 @@ services:
|
|||||||
## OpenID Connect (pre-configured for local tests):
|
## OpenID Connect (pre-configured for local tests):
|
||||||
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
|
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
|
||||||
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"
|
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"
|
||||||
PHOTOPRISM_OIDC_INSECURE: "true"
|
|
||||||
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
|
PHOTOPRISM_OIDC_CLIENT: "photoprism-develop"
|
||||||
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
|
PHOTOPRISM_OIDC_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
|
||||||
|
PHOTOPRISM_OIDC_PROVIDER: "Keycloak"
|
||||||
|
PHOTOPRISM_OIDC_REGISTER: "true"
|
||||||
|
PHOTOPRISM_OIDC_WEBDAV: "true"
|
||||||
|
PHOTOPRISM_DISABLE_OIDC: "false"
|
||||||
## LDAP Authentication (pre-configured for local tests):
|
## LDAP Authentication (pre-configured for local tests):
|
||||||
PHOTOPRISM_LDAP_URI: "ldap://dummy-ldap:389"
|
PHOTOPRISM_LDAP_URI: "ldap://dummy-ldap:389"
|
||||||
PHOTOPRISM_LDAP_INSECURE: "true"
|
PHOTOPRISM_LDAP_INSECURE: "true"
|
||||||
|
|||||||
50
frontend/package-lock.json
generated
50
frontend/package-lock.json
generated
@@ -6467,9 +6467,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.818",
|
"version": "1.4.819",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.818.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.819.tgz",
|
||||||
"integrity": "sha512-eGvIk2V0dGImV9gWLq8fDfTTsCAeMDwZqEPMr+jMInxZdnp9Us8UpovYpRCf9NQ7VOFgrN2doNSgvISbsbNpxA==",
|
"integrity": "sha512-8RwI6gKUokbHWcN3iRij/qpvf/wCbIVY5slODi85werwqUQwpFXM+dvUBND93Qh7SB0pW3Hlq3/wZsqQ3M9Jaw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
@@ -7582,9 +7582,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/esquery": {
|
"node_modules/esquery": {
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
|
||||||
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
|
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"estraverse": "^5.1.0"
|
"estraverse": "^5.1.0"
|
||||||
@@ -8099,9 +8099,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/flow-remove-types": {
|
"node_modules/flow-remove-types": {
|
||||||
"version": "2.239.0",
|
"version": "2.239.1",
|
||||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.239.0.tgz",
|
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.239.1.tgz",
|
||||||
"integrity": "sha512-IBFfuqEe46Iegl4FwSq21cy3MndTTzc6yWboK+CcDyP2wyoMJIy2wbT+FJbissh0z9rRDavv5nNVUqKkty9KOQ==",
|
"integrity": "sha512-Kam48LEA8DGmyG06fsbUA7sXhRppU6Jc903AvCChPfPmiTDAXW7xqWpYsbKFKuypuvc9BJI3tznFK8MI1Ho6oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hermes-parser": "0.22.0",
|
"hermes-parser": "0.22.0",
|
||||||
@@ -9672,15 +9672,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.2.tgz",
|
||||||
"integrity": "sha512-U23pQPDnmYybVkYjObcuYMk43VRlMLLqLI+RdZy8s8WV8WsxO9SnqSroKaluuvcNOdCAlauKszDwd+umbot5Mg==",
|
"integrity": "sha512-qH3nOSj8q/8+Eg8LUPOq3C+6HWkpUioIjDsq1+D4zY91oZvpPttw8GwtF1nReRYKXl+1AORyFqtm2f5Q1SB6/Q==",
|
||||||
"license": "BlueOak-1.0.0",
|
"license": "BlueOak-1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@isaacs/cliui": "^8.0.2"
|
"@isaacs/cliui": "^8.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": "14 >=14.21 || 16 >=16.20 || >=18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@@ -10676,9 +10676,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib/node_modules/glob": {
|
"node_modules/minizlib/node_modules/glob": {
|
||||||
"version": "10.4.3",
|
"version": "10.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.4.tgz",
|
||||||
"integrity": "sha512-Q38SGlYRpVtDBPSWEylRyctn7uDeTp4NQERTLiCT1FqA9JXPYWqAVmQU6qh4r/zMM5ehxTcbaO8EjhWnvEhmyg==",
|
"integrity": "sha512-XsOKvHsu38Xe19ZQupE6N/HENeHQBA05o3hV8labZZT2zYDg1+emxWHnc/Bm9AcCMPXfD6jt+QC7zC5JSFyumw==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"foreground-child": "^3.1.0",
|
"foreground-child": "^3.1.0",
|
||||||
@@ -10692,7 +10692,7 @@
|
|||||||
"glob": "dist/esm/bin.mjs"
|
"glob": "dist/esm/bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": "14 >=14.21 || 16 >=16.20 || 18 || 20 || >=22"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@@ -10714,9 +10714,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/minizlib/node_modules/rimraf": {
|
"node_modules/minizlib/node_modules/rimraf": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.9.tgz",
|
||||||
"integrity": "sha512-XSh0V2/yNhDEi8HwdIefD8MLgs4LQXPag/nEJWs3YUc3Upn+UHa1GyIkEg9xSSNt7HnkO5FjTvmcRzgf+8UZuw==",
|
"integrity": "sha512-3i7b8OcswU6CpU8Ej89quJD4O98id7TtVM5U4Mybh84zQXdrFmDLouWBEEaD/QfO3gDDfH+AGFCGsR7kngzQnA==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^10.3.7"
|
"glob": "^10.3.7"
|
||||||
@@ -10725,7 +10725,7 @@
|
|||||||
"rimraf": "dist/esm/bin.mjs"
|
"rimraf": "dist/esm/bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": "14 >=14.20 || 16 >=16.20 || >=18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
@@ -11491,12 +11491,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-scurry/node_modules/lru-cache": {
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
"version": "10.4.0",
|
"version": "10.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.2.tgz",
|
||||||
"integrity": "sha512-bfJaPTuEiTYBu+ulDaeQ0F+uLmlfFkMgXj4cbwfuMSjgObGMzb55FMMbDvbRU0fAHZ4sLGkz2mKwcMg8Dvm8Ww==",
|
"integrity": "sha512-voV4dDrdVZVNz84n39LFKDaRzfwhdzJ7akpyXfTMxCgRUp07U3lcJUXRlhTKP17rgt09sUzLi5iCitpEAr+6ug==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": "14 || 16 || 18 || 20 || >=22"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class Session extends RestModel {
|
|||||||
ClientName: "",
|
ClientName: "",
|
||||||
AuthProvider: "",
|
AuthProvider: "",
|
||||||
AuthMethod: "",
|
AuthMethod: "",
|
||||||
AuthDomain: "",
|
AuthIssuer: "",
|
||||||
AuthID: "",
|
AuthID: "",
|
||||||
AuthScope: "",
|
AuthScope: "",
|
||||||
GrantType: "",
|
GrantType: "",
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export class User extends RestModel {
|
|||||||
UUID: "",
|
UUID: "",
|
||||||
AuthProvider: "",
|
AuthProvider: "",
|
||||||
AuthMethod: "",
|
AuthMethod: "",
|
||||||
|
AuthIssuer: "",
|
||||||
AuthID: "",
|
AuthID: "",
|
||||||
Name: "",
|
Name: "",
|
||||||
DisplayName: "",
|
DisplayName: "",
|
||||||
@@ -212,6 +213,10 @@ export class User extends RestModel {
|
|||||||
return this.AuthProvider && this.AuthProvider === "ldap";
|
return this.AuthProvider && this.AuthProvider === "ldap";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requiresPassword() {
|
||||||
|
return !this.AuthProvider || this.AuthProvider === "default" || this.AuthProvider === "local";
|
||||||
|
}
|
||||||
|
|
||||||
authInfo() {
|
authInfo() {
|
||||||
if (!this || !this.AuthProvider) {
|
if (!this || !this.AuthProvider) {
|
||||||
return $gettext("Default");
|
return $gettext("Default");
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
<v-flex xs12 class="px-2 py-1">
|
<v-flex xs12 class="px-2 py-1">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="one-time-code"
|
id="one-time-code"
|
||||||
|
key="auth-input-code"
|
||||||
ref="code"
|
ref="code"
|
||||||
v-model="code"
|
v-model="code"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
@@ -45,6 +46,7 @@
|
|||||||
<v-flex xs12 class="pa-2">
|
<v-flex xs12 class="pa-2">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="auth-username"
|
id="auth-username"
|
||||||
|
key="auth-input-username"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
:disabled="loading || enterCode"
|
:disabled="loading || enterCode"
|
||||||
name="username"
|
name="username"
|
||||||
@@ -70,6 +72,7 @@
|
|||||||
<v-flex xs12 class="px-2 py-1">
|
<v-flex xs12 class="px-2 py-1">
|
||||||
<v-text-field
|
<v-text-field
|
||||||
id="auth-password"
|
id="auth-password"
|
||||||
|
key="auth-input-password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find existing user record and update it, if necessary.
|
// Find existing user record and update it, if necessary.
|
||||||
if oidcUser := entity.OidcUser(userInfo, oidc.Username(userInfo, conf.OIDCUsername())); authn.ProviderOIDC.NotEqual(oidcUser.AuthProvider) {
|
if oidcUser := entity.OidcUser(userInfo, provider.Issuer(), oidc.Username(userInfo, conf.OIDCUsername())); authn.ProviderOIDC.NotEqual(oidcUser.AuthProvider) {
|
||||||
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrAuthProviderIsNotOIDC.Error()})
|
event.AuditErr([]string{clientIp, "create session", "oidc", authn.ErrAuthProviderIsNotOIDC.Error()})
|
||||||
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrAuthProviderIsNotOIDC.Error())
|
event.LoginError(clientIp, "oidc", oidcUser.UserName, userAgent, authn.ErrAuthProviderIsNotOIDC.Error())
|
||||||
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
c.HTML(http.StatusUnauthorized, "auth.gohtml", CreateSessionError(http.StatusUnauthorized, i18n.Error(i18n.ErrInvalidCredentials)))
|
||||||
@@ -311,13 +311,13 @@ func OIDCRedirect(router *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update Subject ID (auth_id).
|
// Update Subject ID (auth_id).
|
||||||
user.SetAuthID(userInfo.Subject)
|
user.SetAuthID(userInfo.Subject, provider.Issuer())
|
||||||
|
|
||||||
// Step 2: Create user session.
|
// Step 2: Create user session.
|
||||||
sess := get.Session().New(c)
|
sess := get.Session().New(c)
|
||||||
sess.SetProvider(authn.ProviderOIDC)
|
sess.SetProvider(authn.ProviderOIDC)
|
||||||
sess.SetMethod(authn.MethodDefault)
|
sess.SetMethod(authn.MethodDefault)
|
||||||
sess.SetAuthID(user.AuthID)
|
sess.SetAuthID(user.AuthID, provider.Issuer())
|
||||||
sess.SetUser(user)
|
sess.SetUser(user)
|
||||||
sess.SetGrantType(authn.GrantAuthorizationCode)
|
sess.SetGrantType(authn.GrantAuthorizationCode)
|
||||||
sess.IdToken = tokens.IDToken
|
sess.IdToken = tokens.IDToken
|
||||||
|
|||||||
@@ -23,9 +23,9 @@ func TestAuthListCommand(t *testing.T) {
|
|||||||
// Check command output for plausibility.
|
// Check command output for plausibility.
|
||||||
// t.Logf(output)
|
// t.Logf(output)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, output, "alice")
|
assert.Contains(t, output, "alice ")
|
||||||
assert.Contains(t, output, "bob")
|
assert.Contains(t, output, "bob ")
|
||||||
assert.Contains(t, output, "visitor")
|
assert.Contains(t, output, "visitor ")
|
||||||
})
|
})
|
||||||
t.Run("Alice", func(t *testing.T) {
|
t.Run("Alice", func(t *testing.T) {
|
||||||
var err error
|
var err error
|
||||||
@@ -42,9 +42,9 @@ func TestAuthListCommand(t *testing.T) {
|
|||||||
// t.Logf(output)
|
// t.Logf(output)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, output, "Session ID")
|
assert.Contains(t, output, "Session ID")
|
||||||
assert.Contains(t, output, "alice")
|
assert.Contains(t, output, "alice ")
|
||||||
assert.NotContains(t, output, "bob")
|
assert.NotContains(t, output, "bob ")
|
||||||
assert.NotContains(t, output, "visitor")
|
assert.NotContains(t, output, "visitor ")
|
||||||
assert.NotContains(t, output, "| Preview Token |")
|
assert.NotContains(t, output, "| Preview Token |")
|
||||||
})
|
})
|
||||||
t.Run("CSV", func(t *testing.T) {
|
t.Run("CSV", func(t *testing.T) {
|
||||||
@@ -62,8 +62,8 @@ func TestAuthListCommand(t *testing.T) {
|
|||||||
//t.Logf(output)
|
//t.Logf(output)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, output, "Session ID;")
|
assert.Contains(t, output, "Session ID;")
|
||||||
assert.Contains(t, output, "alice")
|
assert.Contains(t, output, "alice;")
|
||||||
assert.NotContains(t, output, "bob")
|
assert.NotContains(t, output, "bob;")
|
||||||
assert.NotContains(t, output, "visitor")
|
assert.NotContains(t, output, "visitor")
|
||||||
})
|
})
|
||||||
t.Run("Tokens", func(t *testing.T) {
|
t.Run("Tokens", func(t *testing.T) {
|
||||||
@@ -82,8 +82,8 @@ func TestAuthListCommand(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, output, "| Session ID |")
|
assert.Contains(t, output, "| Session ID |")
|
||||||
assert.Contains(t, output, "| Preview Token |")
|
assert.Contains(t, output, "| Preview Token |")
|
||||||
assert.Contains(t, output, "alice")
|
assert.Contains(t, output, "alice ")
|
||||||
assert.NotContains(t, output, "bob")
|
assert.NotContains(t, output, "bob ")
|
||||||
assert.NotContains(t, output, "visitor")
|
assert.NotContains(t, output, "visitor")
|
||||||
})
|
})
|
||||||
t.Run("NoResult", func(t *testing.T) {
|
t.Run("NoResult", func(t *testing.T) {
|
||||||
|
|||||||
@@ -4,14 +4,17 @@ import (
|
|||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/auth/acl"
|
"github.com/photoprism/photoprism/internal/auth/acl"
|
||||||
|
"github.com/photoprism/photoprism/pkg/authn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Usage hints for the user management subcommands.
|
// Usage hints for the user management subcommands.
|
||||||
const (
|
const (
|
||||||
UserNameUsage = "full `NAME` for display in the interface"
|
UserNameUsage = "full `NAME` for display in the interface"
|
||||||
UserEmailUsage = "unique `EMAIL` address of the user"
|
UserEmailUsage = "unique `EMAIL` address of the user"
|
||||||
UserPasswordUsage = "`PASSWORD` for local authentication"
|
UserPasswordUsage = "`PASSWORD` for local authentication (8-72 characters)"
|
||||||
UserRoleUsage = "user role `NAME` (leave blank for default)"
|
UserRoleUsage = "user account `ROLE` (admin or guest)"
|
||||||
|
UserAuthUsage = "authentication `PROVIDER` (default, local, oidc or none)"
|
||||||
|
UserAuthIDUsage = "authentication `ID` e.g. Subject ID or Distinguished Name (DN)"
|
||||||
UserAdminUsage = "make user super admin with full access"
|
UserAdminUsage = "make user super admin with full access"
|
||||||
UserNoLoginUsage = "disable login on the web interface"
|
UserNoLoginUsage = "disable login on the web interface"
|
||||||
UserWebDAVUsage = "allow to sync files via WebDAV"
|
UserWebDAVUsage = "allow to sync files via WebDAV"
|
||||||
@@ -53,6 +56,16 @@ var UserFlags = []cli.Flag{
|
|||||||
Usage: UserRoleUsage,
|
Usage: UserRoleUsage,
|
||||||
Value: acl.RoleAdmin.String(),
|
Value: acl.RoleAdmin.String(),
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "auth, A",
|
||||||
|
Usage: UserAuthUsage,
|
||||||
|
Value: authn.ProviderDefault.String(),
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "auth-id",
|
||||||
|
Usage: UserAuthIDUsage,
|
||||||
|
Value: "",
|
||||||
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "superadmin, s",
|
Name: "superadmin, s",
|
||||||
Usage: UserAdminUsage,
|
Usage: UserAdminUsage,
|
||||||
|
|||||||
@@ -77,7 +77,8 @@ func usersAddAction(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && frm.UserEmail == "" {
|
// Enter account email.
|
||||||
|
if interactive && frm.UserEmail == "" && frm.Provider().SupportsPasswordAuthentication() {
|
||||||
prompt := promptui.Prompt{
|
prompt := promptui.Prompt{
|
||||||
Label: "Email",
|
Label: "Email",
|
||||||
}
|
}
|
||||||
@@ -91,7 +92,9 @@ func usersAddAction(ctx *cli.Context) error {
|
|||||||
frm.UserEmail = clean.Email(res)
|
frm.UserEmail = clean.Email(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
if interactive && len([]rune(ctx.String("password"))) < entity.PasswordLength {
|
// Enter account password.
|
||||||
|
if interactive && (frm.Provider().RequiresLocalPassword() || frm.Password != "") &&
|
||||||
|
len([]rune(ctx.String("password"))) < entity.PasswordLength {
|
||||||
validate := func(input string) error {
|
validate := func(input string) error {
|
||||||
if len([]rune(input)) < entity.PasswordLength {
|
if len([]rune(input)) < entity.PasswordLength {
|
||||||
return fmt.Errorf("password must have at least %d characters", entity.PasswordLength)
|
return fmt.Errorf("password must have at least %d characters", entity.PasswordLength)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ type Session struct {
|
|||||||
client *Client `gorm:"-" yaml:"-"`
|
client *Client `gorm:"-" yaml:"-"`
|
||||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
||||||
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
||||||
AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"AuthDomain" yaml:"AuthDomain,omitempty"`
|
AuthIssuer string `gorm:"type:VARBINARY(255);default:'';" json:"AuthIssuer,omitempty" yaml:"AuthIssuer,omitempty"`
|
||||||
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
||||||
AuthScope string `gorm:"size:1024;default:'';" json:"AuthScope" yaml:"AuthScope,omitempty"`
|
AuthScope string `gorm:"size:1024;default:'';" json:"AuthScope" yaml:"AuthScope,omitempty"`
|
||||||
GrantType string `gorm:"type:VARBINARY(64);default:'';" json:"GrantType" yaml:"GrantType,omitempty"`
|
GrantType string `gorm:"type:VARBINARY(64);default:'';" json:"GrantType" yaml:"GrantType,omitempty"`
|
||||||
@@ -432,12 +432,13 @@ func (m *Session) AuthInfo() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetAuthID sets a custom authentication identifier.
|
// SetAuthID sets a custom authentication identifier.
|
||||||
func (m *Session) SetAuthID(id string) *Session {
|
func (m *Session) SetAuthID(id, issuer string) *Session {
|
||||||
if id == "" {
|
if id == "" {
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
m.AuthID = clean.Auth(id)
|
m.AuthID = clean.Auth(id)
|
||||||
|
m.AuthIssuer = clean.Uri(issuer)
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func AuthLocal(user *User, f form.Login, s *Session, c *gin.Context) (provider a
|
|||||||
s.SetMethod(method)
|
s.SetMethod(method)
|
||||||
|
|
||||||
// Set auth ID to the ID of the parent session.
|
// Set auth ID to the ID of the parent session.
|
||||||
s.SetAuthID(authSess.ID)
|
s.SetAuthID(authSess.ID, user.UserUID)
|
||||||
|
|
||||||
// Limit lifetime of the session to that of the parent session, if any.
|
// Limit lifetime of the session to that of the parent session, if any.
|
||||||
if authSess.SessExpires > 0 && s.SessExpires > authSess.SessExpires {
|
if authSess.SessExpires > 0 && s.SessExpires > authSess.SessExpires {
|
||||||
|
|||||||
@@ -491,9 +491,10 @@ func TestSession_SetAuthID(t *testing.T) {
|
|||||||
AuthID: "test-session-auth-id",
|
AuthID: "test-session-auth-id",
|
||||||
}
|
}
|
||||||
|
|
||||||
m := s.SetAuthID("")
|
m := s.SetAuthID("", "https://accounts.google.com")
|
||||||
|
|
||||||
assert.Equal(t, "test-session-auth-id", m.AuthID)
|
assert.Equal(t, "test-session-auth-id", m.AuthID)
|
||||||
|
assert.Equal(t, "", m.AuthIssuer)
|
||||||
})
|
})
|
||||||
t.Run("New", func(t *testing.T) {
|
t.Run("New", func(t *testing.T) {
|
||||||
s := &Session{
|
s := &Session{
|
||||||
@@ -502,9 +503,10 @@ func TestSession_SetAuthID(t *testing.T) {
|
|||||||
AuthID: "new-id",
|
AuthID: "new-id",
|
||||||
}
|
}
|
||||||
|
|
||||||
m := s.SetAuthID("new-id")
|
m := s.SetAuthID("new-id", "https://accounts.google.com")
|
||||||
|
|
||||||
assert.Equal(t, "new-id", m.AuthID)
|
assert.Equal(t, "new-id", m.AuthID)
|
||||||
|
assert.Equal(t, "https://accounts.google.com", m.AuthIssuer)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ type User struct {
|
|||||||
UserUID string `gorm:"type:VARBINARY(42);column:user_uid;unique_index;" json:"UID" yaml:"UID"`
|
UserUID string `gorm:"type:VARBINARY(42);column:user_uid;unique_index;" json:"UID" yaml:"UID"`
|
||||||
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"AuthProvider" yaml:"AuthProvider,omitempty"`
|
||||||
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"AuthMethod" yaml:"AuthMethod,omitempty"`
|
||||||
|
AuthIssuer string `gorm:"type:VARBINARY(255);default:'';" json:"AuthIssuer,omitempty" yaml:"AuthIssuer,omitempty"`
|
||||||
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
AuthID string `gorm:"type:VARBINARY(255);index;default:'';" json:"AuthID" yaml:"AuthID,omitempty"`
|
||||||
UserName string `gorm:"size:200;index;" json:"Name" yaml:"Name,omitempty"`
|
UserName string `gorm:"size:200;index;" json:"Name" yaml:"Name,omitempty"`
|
||||||
DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
DisplayName string `gorm:"size:200;" json:"DisplayName" yaml:"DisplayName,omitempty"`
|
||||||
@@ -105,7 +106,7 @@ func NewUser() (m *User) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// OidcUser creates a new OIDC user entity.
|
// OidcUser creates a new OIDC user entity.
|
||||||
func OidcUser(userInfo *oidc.UserInfo, userName string) User {
|
func OidcUser(userInfo *oidc.UserInfo, issuer, userName string) User {
|
||||||
authId := clean.Auth(userInfo.Subject)
|
authId := clean.Auth(userInfo.Subject)
|
||||||
|
|
||||||
if authId == "" {
|
if authId == "" {
|
||||||
@@ -117,6 +118,7 @@ func OidcUser(userInfo *oidc.UserInfo, userName string) User {
|
|||||||
DisplayName: userInfo.Name,
|
DisplayName: userInfo.Name,
|
||||||
UserEmail: clean.Email(userInfo.Email),
|
UserEmail: clean.Email(userInfo.Email),
|
||||||
AuthProvider: authn.ProviderOIDC.String(),
|
AuthProvider: authn.ProviderOIDC.String(),
|
||||||
|
AuthIssuer: issuer,
|
||||||
AuthID: authId,
|
AuthID: authId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,7 +145,11 @@ func FindUser(find User) *User {
|
|||||||
} else if rnd.IsUID(find.UserUID, UserUID) {
|
} else if rnd.IsUID(find.UserUID, UserUID) {
|
||||||
stmt = stmt.Where("user_uid = ?", find.UserUID)
|
stmt = stmt.Where("user_uid = ?", find.UserUID)
|
||||||
} else if authn.ProviderOIDC.Equal(find.AuthProvider) && find.AuthID != "" {
|
} else if authn.ProviderOIDC.Equal(find.AuthProvider) && find.AuthID != "" {
|
||||||
stmt = stmt.Where("auth_provider = ? AND auth_id = ?", find.AuthProvider, find.AuthID)
|
if find.AuthIssuer == "" {
|
||||||
|
stmt = stmt.Where("auth_provider = ? AND auth_id = ?", find.AuthProvider, find.AuthID)
|
||||||
|
} else {
|
||||||
|
stmt = stmt.Where("auth_provider = ? AND (auth_issuer = '' OR auth_issuer = ?) AND auth_id = ?", find.AuthProvider, find.AuthIssuer, find.AuthID)
|
||||||
|
}
|
||||||
} else if find.AuthProvider != "" && find.AuthID != "" && find.UserName != "" {
|
} else if find.AuthProvider != "" && find.AuthID != "" && find.UserName != "" {
|
||||||
stmt = stmt.Where("auth_provider = ? AND auth_id = ? OR user_name = ?", find.AuthProvider, find.AuthID, find.UserName)
|
stmt = stmt.Where("auth_provider = ? AND auth_id = ? OR user_name = ?", find.AuthProvider, find.AuthID, find.UserName)
|
||||||
} else if find.UserName != "" {
|
} else if find.UserName != "" {
|
||||||
@@ -596,7 +602,7 @@ func (m *User) SetMethod(method authn.MethodType) *User {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SetAuthID sets a custom authentication identifier.
|
// SetAuthID sets a custom authentication identifier.
|
||||||
func (m *User) SetAuthID(id string) *User {
|
func (m *User) SetAuthID(id, issuer string) *User {
|
||||||
if m == nil {
|
if m == nil {
|
||||||
return &User{}
|
return &User{}
|
||||||
}
|
}
|
||||||
@@ -606,6 +612,7 @@ func (m *User) SetAuthID(id string) *User {
|
|||||||
return m
|
return m
|
||||||
} else {
|
} else {
|
||||||
m.AuthID = authId
|
m.AuthID = authId
|
||||||
|
m.AuthIssuer = clean.Uri(issuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure other users do not use the same identifier.
|
// Make sure other users do not use the same identifier.
|
||||||
@@ -1081,6 +1088,8 @@ func (m *User) Validate() (err error) {
|
|||||||
func (m *User) SetFormValues(frm form.User) *User {
|
func (m *User) SetFormValues(frm form.User) *User {
|
||||||
m.UserName = frm.Username()
|
m.UserName = frm.Username()
|
||||||
m.SetProvider(frm.Provider())
|
m.SetProvider(frm.Provider())
|
||||||
|
m.SetMethod(frm.Method())
|
||||||
|
m.SetAuthID(frm.AuthID, frm.AuthIssuer)
|
||||||
m.UserEmail = frm.Email()
|
m.UserEmail = frm.Email()
|
||||||
m.DisplayName = frm.DisplayName
|
m.DisplayName = frm.DisplayName
|
||||||
m.SuperAdmin = frm.SuperAdmin
|
m.SuperAdmin = frm.SuperAdmin
|
||||||
@@ -1224,6 +1233,7 @@ func (m *User) PrivilegeLevelChange(f form.User) bool {
|
|||||||
m.UserAttr != f.Attr() ||
|
m.UserAttr != f.Attr() ||
|
||||||
m.AuthProvider != f.AuthProvider ||
|
m.AuthProvider != f.AuthProvider ||
|
||||||
m.AuthMethod != f.AuthMethod ||
|
m.AuthMethod != f.AuthMethod ||
|
||||||
|
m.AuthIssuer != f.AuthIssuer ||
|
||||||
m.AuthID != f.AuthID ||
|
m.AuthID != f.AuthID ||
|
||||||
m.BasePath != f.BasePath ||
|
m.BasePath != f.BasePath ||
|
||||||
m.UploadPath != f.UploadPath
|
m.UploadPath != f.UploadPath
|
||||||
@@ -1290,7 +1300,7 @@ func (m *User) SaveForm(f form.User, u *User) error {
|
|||||||
if u.IsSuperAdmin() {
|
if u.IsSuperAdmin() {
|
||||||
m.SetProvider(f.Provider())
|
m.SetProvider(f.Provider())
|
||||||
m.SetMethod(f.Method())
|
m.SetMethod(f.Method())
|
||||||
m.SetAuthID(f.AuthID)
|
m.SetAuthID(f.AuthID, f.AuthIssuer)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.SetBasePath(f.BasePath)
|
m.SetBasePath(f.BasePath)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/jinzhu/gorm"
|
"github.com/jinzhu/gorm"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/form"
|
"github.com/photoprism/photoprism/internal/form"
|
||||||
|
"github.com/photoprism/photoprism/pkg/authn"
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
@@ -14,12 +15,16 @@ import (
|
|||||||
func AddUser(frm form.User) error {
|
func AddUser(frm form.User) error {
|
||||||
user := NewUser().SetFormValues(frm)
|
user := NewUser().SetFormValues(frm)
|
||||||
|
|
||||||
if len([]rune(frm.Password)) < PasswordLength {
|
// Check auth id and password.
|
||||||
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
if authId := clean.Auth(frm.AuthID); frm.Provider().Is(authn.ProviderOIDC) && len(authId) < 4 {
|
||||||
|
return authn.ErrAuthIDRequired
|
||||||
} else if len(frm.Password) > txt.ClipPassword {
|
} else if len(frm.Password) > txt.ClipPassword {
|
||||||
return fmt.Errorf("password must have less than %d characters", txt.ClipPassword)
|
return fmt.Errorf("password must have less than %d characters", txt.ClipPassword)
|
||||||
|
} else if (frm.Provider().RequiresLocalPassword() || frm.Password != "") && len([]rune(frm.Password)) < PasswordLength {
|
||||||
|
return fmt.Errorf("password must have at least %d characters", PasswordLength)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check username, role and email.
|
||||||
if err := user.Validate(); err != nil {
|
if err := user.Validate(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -29,10 +34,12 @@ func AddUser(frm form.User) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
pw := NewPassword(user.UserUID, frm.Password, false)
|
if frm.Password != "" {
|
||||||
|
pw := NewPassword(user.UserUID, frm.Password, false)
|
||||||
|
|
||||||
if err := tx.Create(&pw).Error; err != nil {
|
if err := tx.Create(&pw).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("successfully added user %s", clean.LogQuote(user.Username()))
|
log.Infof("successfully added user %s", clean.LogQuote(user.Username()))
|
||||||
|
|||||||
@@ -31,6 +31,23 @@ func (m *User) SetValuesFromCli(ctx *cli.Context) error {
|
|||||||
privilegeLevelChange = true
|
privilegeLevelChange = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Authentication Provider.
|
||||||
|
if ctx.IsSet("auth") {
|
||||||
|
m.SetProvider(frm.Provider())
|
||||||
|
privilegeLevelChange = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authentication ID.
|
||||||
|
if ctx.IsSet("auth-id") {
|
||||||
|
if frm.AuthID == "" {
|
||||||
|
m.AuthID = ""
|
||||||
|
m.AuthIssuer = ""
|
||||||
|
} else {
|
||||||
|
m.SetAuthID(frm.AuthID, m.AuthIssuer)
|
||||||
|
}
|
||||||
|
privilegeLevelChange = true
|
||||||
|
}
|
||||||
|
|
||||||
// Super-admin status.
|
// Super-admin status.
|
||||||
if ctx.IsSet("superadmin") {
|
if ctx.IsSet("superadmin") {
|
||||||
m.SuperAdmin = frm.SuperAdmin
|
m.SuperAdmin = frm.SuperAdmin
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ func TestOidcUser(t *testing.T) {
|
|||||||
info.Subject = "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d"
|
info.Subject = "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d"
|
||||||
info.PreferredUsername = "Jane Doe"
|
info.PreferredUsername = "Jane Doe"
|
||||||
|
|
||||||
m := OidcUser(info, "jane.doe")
|
m := OidcUser(info, "", "jane.doe")
|
||||||
|
|
||||||
assert.Equal(t, "oidc", m.AuthProvider)
|
assert.Equal(t, "oidc", m.AuthProvider)
|
||||||
|
assert.Equal(t, "", m.AuthIssuer)
|
||||||
assert.Equal(t, "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d", m.AuthID)
|
assert.Equal(t, "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d", m.AuthID)
|
||||||
assert.Equal(t, "jane@doe.com", m.UserEmail)
|
assert.Equal(t, "jane@doe.com", m.UserEmail)
|
||||||
assert.Equal(t, "jane.doe", m.UserName)
|
assert.Equal(t, "jane.doe", m.UserName)
|
||||||
@@ -49,9 +50,10 @@ func TestOidcUser(t *testing.T) {
|
|||||||
info.Subject = "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d"
|
info.Subject = "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d"
|
||||||
info.PreferredUsername = "Jane Doe"
|
info.PreferredUsername = "Jane Doe"
|
||||||
|
|
||||||
m := OidcUser(info, "")
|
m := OidcUser(info, "https://accounts.google.com", "")
|
||||||
|
|
||||||
assert.Equal(t, "oidc", m.AuthProvider)
|
assert.Equal(t, "oidc", m.AuthProvider)
|
||||||
|
assert.Equal(t, "https://accounts.google.com", m.AuthIssuer)
|
||||||
assert.Equal(t, "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d", m.AuthID)
|
assert.Equal(t, "e3a9f4a6-9d60-47cb-9bf5-02bd15b0c68d", m.AuthID)
|
||||||
assert.Equal(t, "jane@doe.com", m.UserEmail)
|
assert.Equal(t, "jane@doe.com", m.UserEmail)
|
||||||
assert.Equal(t, "", m.UserName)
|
assert.Equal(t, "", m.UserName)
|
||||||
@@ -67,9 +69,10 @@ func TestOidcUser(t *testing.T) {
|
|||||||
info.EmailVerified = true
|
info.EmailVerified = true
|
||||||
info.Subject = ""
|
info.Subject = ""
|
||||||
|
|
||||||
m := OidcUser(info, "jane.doe")
|
m := OidcUser(info, "https://accounts.google.com", "jane.doe")
|
||||||
|
|
||||||
assert.Equal(t, "", m.AuthProvider)
|
assert.Equal(t, "", m.AuthProvider)
|
||||||
|
assert.Equal(t, "", m.AuthIssuer)
|
||||||
assert.Equal(t, "", m.AuthID)
|
assert.Equal(t, "", m.AuthID)
|
||||||
assert.Equal(t, "", m.UserEmail)
|
assert.Equal(t, "", m.UserEmail)
|
||||||
assert.Equal(t, "", m.UserName)
|
assert.Equal(t, "", m.UserName)
|
||||||
|
|||||||
@@ -177,4 +177,10 @@ var DialectMySQL = Migrations{
|
|||||||
Stage: "main",
|
Stage: "main",
|
||||||
Statements: []string{"ALTER TABLE auth_sessions MODIFY IF EXISTS refresh_token VARBINARY(2048);", "ALTER TABLE auth_sessions MODIFY IF EXISTS id_token VARBINARY(2048);"},
|
Statements: []string{"ALTER TABLE auth_sessions MODIFY IF EXISTS refresh_token VARBINARY(2048);", "ALTER TABLE auth_sessions MODIFY IF EXISTS id_token VARBINARY(2048);"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "20240709-000001",
|
||||||
|
Dialect: "mysql",
|
||||||
|
Stage: "pre",
|
||||||
|
Statements: []string{"ALTER IGNORE TABLE auth_sessions RENAME COLUMN auth_domain TO auth_issuer;"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,4 +99,10 @@ var DialectSQLite3 = Migrations{
|
|||||||
Stage: "main",
|
Stage: "main",
|
||||||
Statements: []string{"DELETE FROM auth_sessions;"},
|
Statements: []string{"DELETE FROM auth_sessions;"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ID: "20240709-000001",
|
||||||
|
Dialect: "sqlite3",
|
||||||
|
Stage: "pre",
|
||||||
|
Statements: []string{"ALTER TABLE auth_sessions RENAME COLUMN auth_domain TO auth_issuer;"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
1
internal/entity/migrate/mysql/20240709-000001.pre.sql
Normal file
1
internal/entity/migrate/mysql/20240709-000001.pre.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER IGNORE TABLE auth_sessions RENAME COLUMN auth_domain TO auth_issuer;
|
||||||
1
internal/entity/migrate/sqlite3/20240709-000001.pre.sql
Normal file
1
internal/entity/migrate/sqlite3/20240709-000001.pre.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE auth_sessions RENAME COLUMN auth_domain TO auth_issuer;
|
||||||
@@ -12,7 +12,8 @@ type User struct {
|
|||||||
UserName string `json:"Name,omitempty" yaml:"Name,omitempty"`
|
UserName string `json:"Name,omitempty" yaml:"Name,omitempty"`
|
||||||
AuthProvider string `json:"AuthProvider,omitempty" yaml:"AuthProvider,omitempty"`
|
AuthProvider string `json:"AuthProvider,omitempty" yaml:"AuthProvider,omitempty"`
|
||||||
AuthMethod string `json:"AuthMethod,omitempty" yaml:"AuthMethod,omitempty"`
|
AuthMethod string `json:"AuthMethod,omitempty" yaml:"AuthMethod,omitempty"`
|
||||||
AuthID string `json:"AuthID,omitempty" yaml:"AuthMethod,omitempty"`
|
AuthIssuer string `json:"AuthIssuer,omitempty" yaml:"AuthIssuer,omitempty"`
|
||||||
|
AuthID string `json:"AuthID,omitempty" yaml:"AuthID,omitempty"`
|
||||||
UserEmail string `json:"Email,omitempty" yaml:"Email,omitempty"`
|
UserEmail string `json:"Email,omitempty" yaml:"Email,omitempty"`
|
||||||
DisplayName string `json:"DisplayName,omitempty" yaml:"DisplayName,omitempty"`
|
DisplayName string `json:"DisplayName,omitempty" yaml:"DisplayName,omitempty"`
|
||||||
UserRole string `json:"Role,omitempty" yaml:"Role,omitempty"`
|
UserRole string `json:"Role,omitempty" yaml:"Role,omitempty"`
|
||||||
@@ -31,6 +32,7 @@ func NewUserFromCli(ctx *cli.Context) User {
|
|||||||
return User{
|
return User{
|
||||||
UserName: clean.Username(ctx.Args().First()),
|
UserName: clean.Username(ctx.Args().First()),
|
||||||
AuthProvider: clean.TypeLower(ctx.String("auth")),
|
AuthProvider: clean.TypeLower(ctx.String("auth")),
|
||||||
|
AuthID: clean.Auth(ctx.String("auth-id")),
|
||||||
UserEmail: clean.Email(ctx.String("email")),
|
UserEmail: clean.Email(ctx.String("email")),
|
||||||
DisplayName: clean.Name(ctx.String("name")),
|
DisplayName: clean.Name(ctx.String("name")),
|
||||||
UserRole: clean.Role(ctx.String("role")),
|
UserRole: clean.Role(ctx.String("role")),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ var (
|
|||||||
ErrInvalidClientID = errors.New("invalid client id")
|
ErrInvalidClientID = errors.New("invalid client id")
|
||||||
ErrInvalidAuthID = errors.New("invalid auth id")
|
ErrInvalidAuthID = errors.New("invalid auth id")
|
||||||
ErrAuthProviderIsNotOIDC = errors.New("auth provider is not oidc")
|
ErrAuthProviderIsNotOIDC = errors.New("auth provider is not oidc")
|
||||||
|
ErrAuthIDRequired = errors.New("auth id required")
|
||||||
ErrAuthCodeRequired = errors.New("auth code required")
|
ErrAuthCodeRequired = errors.New("auth code required")
|
||||||
ErrClientIDRequired = errors.New("client id required")
|
ErrClientIDRequired = errors.New("client id required")
|
||||||
ErrInvalidClientSecret = errors.New("invalid client secret")
|
ErrInvalidClientSecret = errors.New("invalid client secret")
|
||||||
|
|||||||
15
pkg/authn/issuer.go
Normal file
15
pkg/authn/issuer.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package authn
|
||||||
|
|
||||||
|
import "github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
|
||||||
|
// IssuerUri represents an authentication issuer URI.
|
||||||
|
type IssuerUri = string
|
||||||
|
|
||||||
|
const (
|
||||||
|
IssuerDefault IssuerUri = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
// Issuer returns a sanitized issuer URI.
|
||||||
|
func Issuer(uri string) IssuerUri {
|
||||||
|
return clean.Uri(uri)
|
||||||
|
}
|
||||||
16
pkg/authn/issuer_test.go
Normal file
16
pkg/authn/issuer_test.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package authn
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIssuer(t *testing.T) {
|
||||||
|
assert.Equal(t, "", Issuer(""))
|
||||||
|
assert.Equal(t, "", Issuer(" "))
|
||||||
|
assert.Equal(t, "https://accounts.google.com", Issuer("https://accounts.google.com"))
|
||||||
|
assert.Equal(t, "user://123456", Issuer("user://123456"))
|
||||||
|
assert.Equal(t, "issuer.example.com", Issuer("issuer.example.com"))
|
||||||
|
assert.Equal(t, "example", Issuer(" example "))
|
||||||
|
}
|
||||||
@@ -31,11 +31,11 @@ var LocalProviders = list.List{
|
|||||||
string(ProviderOIDC),
|
string(ProviderOIDC),
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientProviders contains all client authentication providers.
|
// LocalPasswordRequiredProviders contains authentication providers which require a local password.
|
||||||
var ClientProviders = list.List{
|
var LocalPasswordRequiredProviders = list.List{
|
||||||
string(ProviderClient),
|
string(ProviderUndefined),
|
||||||
string(ProviderApplication),
|
string(ProviderDefault),
|
||||||
string(ProviderAccessToken),
|
string(ProviderLocal),
|
||||||
}
|
}
|
||||||
|
|
||||||
// PasswordProviders contains authentication providers which support password authentication (local and remote).
|
// PasswordProviders contains authentication providers which support password authentication (local and remote).
|
||||||
@@ -52,6 +52,13 @@ var PasscodeProviders = list.List{
|
|||||||
string(ProviderLDAP),
|
string(ProviderLDAP),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ClientProviders contains all client authentication providers.
|
||||||
|
var ClientProviders = list.List{
|
||||||
|
string(ProviderClient),
|
||||||
|
string(ProviderApplication),
|
||||||
|
string(ProviderAccessToken),
|
||||||
|
}
|
||||||
|
|
||||||
// Provider casts a string to a normalized provider type.
|
// Provider casts a string to a normalized provider type.
|
||||||
func Provider(s string) ProviderType {
|
func Provider(s string) ProviderType {
|
||||||
s = clean.TypeLowerUnderscore(s)
|
s = clean.TypeLowerUnderscore(s)
|
||||||
@@ -144,6 +151,11 @@ func (t ProviderType) IsUndefined() bool {
|
|||||||
return t == ""
|
return t == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsOIDC checks if the provider is OpenID Connect (OIDC).
|
||||||
|
func (t ProviderType) IsOIDC() bool {
|
||||||
|
return t == ProviderOIDC
|
||||||
|
}
|
||||||
|
|
||||||
// IsLocal checks if local authentication is possible.
|
// IsLocal checks if local authentication is possible.
|
||||||
func (t ProviderType) IsLocal() bool {
|
func (t ProviderType) IsLocal() bool {
|
||||||
return list.Contains(LocalProviders, string(t))
|
return list.Contains(LocalProviders, string(t))
|
||||||
@@ -164,6 +176,11 @@ func (t ProviderType) IsDefault() bool {
|
|||||||
return t.String() == ProviderDefault.String()
|
return t.String() == ProviderDefault.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequiresLocalPassword checks if the provider allows a password to be checked for authentication.
|
||||||
|
func (t ProviderType) RequiresLocalPassword() bool {
|
||||||
|
return list.Contains(LocalPasswordRequiredProviders, string(t))
|
||||||
|
}
|
||||||
|
|
||||||
// SupportsPasswordAuthentication checks if the provider allows a password to be checked for authentication.
|
// SupportsPasswordAuthentication checks if the provider allows a password to be checked for authentication.
|
||||||
func (t ProviderType) SupportsPasswordAuthentication() bool {
|
func (t ProviderType) SupportsPasswordAuthentication() bool {
|
||||||
return list.Contains(PasswordProviders, string(t))
|
return list.Contains(PasswordProviders, string(t))
|
||||||
|
|||||||
Reference in New Issue
Block a user