Merge pull request #2769 from Sapd/openid-permissions

OpenID: Integrate permissions (Fixes #2523)
This commit is contained in:
advplyr 2024-03-30 14:38:32 -05:00 committed by GitHub
commit a9c9c447f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 422 additions and 61 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-4 mb-8"> <div class="bg-bg rounded-md shadow-lg border border-white border-opacity-5 p-2 sm:p-4 mb-8">
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<slot name="header-prefix"></slot> <slot name="header-prefix"></slot>
<h1 class="text-xl">{{ headerText }}</h1> <h1 class="text-xl">{{ headerText }}</h1>

View File

@ -5,7 +5,7 @@
>{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label >{{ label }}<em v-if="note" class="font-normal text-xs pl-2">{{ note }}</em></label
> >
</slot> </slot>
<ui-text-input :placeholder="label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" /> <ui-text-input :placeholder="placeholder || label" :inputId="identifier" ref="input" v-model="inputValue" :disabled="disabled" :readonly="readonly" :type="type" class="w-full" :class="inputClass" @blur="inputBlurred" />
</div> </div>
</template> </template>
@ -14,6 +14,7 @@ export default {
props: { props: {
value: [String, Number], value: [String, Number],
label: String, label: String,
placeholder: String,
note: String, note: String,
type: { type: {
type: String, type: String,

View File

@ -59,28 +59,49 @@
<ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" /> <ui-text-input-with-label ref="openidClientSecret" v-model="newAuthSettings.authOpenIDClientSecret" :disabled="savingSettings" :label="'Client Secret'" class="mb-2" />
<ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" /> <ui-multi-select ref="redirectUris" v-model="newAuthSettings.authOpenIDMobileRedirectURIs" :items="newAuthSettings.authOpenIDMobileRedirectURIs" :label="$strings.LabelMobileRedirectURIs" class="mb-2" :menuDisabled="true" :disabled="savingSettings" />
<p class="pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" /> <p class="sm:pl-4 text-sm text-gray-300 mb-2" v-html="$strings.LabelMobileRedirectURIsDescription" />
<ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" /> <ui-text-input-with-label ref="buttonTextInput" v-model="newAuthSettings.authOpenIDButtonText" :disabled="savingSettings" :label="$strings.LabelButtonText" class="mb-2" />
<div class="flex items-center pt-1 mb-2"> <div class="flex sm:items-center flex-col sm:flex-row pt-1 mb-2">
<div class="w-44"> <div class="w-44">
<ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" :label="$strings.LabelMatchExistingUsersBy" :disabled="savingSettings" /> <ui-dropdown v-model="newAuthSettings.authOpenIDMatchExistingBy" small :items="matchingExistingOptions" :label="$strings.LabelMatchExistingUsersBy" :disabled="savingSettings" />
</div> </div>
<p class="pl-4 text-sm text-gray-300 mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p> <p class="sm:pl-4 text-sm text-gray-300 mt-2 sm:mt-5">{{ $strings.LabelMatchExistingUsersByDescription }}</p>
</div> </div>
<div class="flex items-center py-4 px-1"> <div class="flex items-center py-4 px-1 w-full">
<ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" /> <ui-toggle-switch labeledBy="auto-redirect-toggle" v-model="newAuthSettings.authOpenIDAutoLaunch" :disabled="savingSettings" />
<p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoLaunch }}</p> <p id="auto-redirect-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoLaunch }}</p>
<p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelAutoLaunchDescription" /> <p class="pl-4 text-sm text-gray-300" v-html="$strings.LabelAutoLaunchDescription" />
</div> </div>
<div class="flex items-center py-4 px-1"> <div class="flex items-center py-4 px-1 w-full">
<ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" /> <ui-toggle-switch labeledBy="auto-register-toggle" v-model="newAuthSettings.authOpenIDAutoRegister" :disabled="savingSettings" />
<p id="auto-register-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoRegister }}</p> <p id="auto-register-toggle" class="pl-4 whitespace-nowrap">{{ $strings.LabelAutoRegister }}</p>
<p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p> <p class="pl-4 text-sm text-gray-300">{{ $strings.LabelAutoRegisterDescription }}</p>
</div> </div>
<p class="pt-6 mb-4 px-1">{{ $strings.LabelOpenIDClaims }}</p>
<div class="flex flex-col sm:flex-row mb-4">
<div class="w-44 min-w-44">
<ui-text-input-with-label ref="openidGroupClaim" v-model="newAuthSettings.authOpenIDGroupClaim" :disabled="savingSettings" :placeholder="'groups'" :label="'Group Claim'" />
</div>
<p class="sm:pl-4 pt-2 sm:pt-0 text-sm text-gray-300" v-html="$strings.LabelOpenIDGroupClaimDescription"></p>
</div>
<div class="flex flex-col sm:flex-row mb-4">
<div class="w-44 min-w-44">
<ui-text-input-with-label ref="openidAdvancedPermsClaim" v-model="newAuthSettings.authOpenIDAdvancedPermsClaim" :disabled="savingSettings" :placeholder="'abspermissions'" :label="'Advanced Permission Claim'" />
</div>
<div class="sm:pl-4 pt-2 sm:pt-0 text-sm text-gray-300">
<p v-html="$strings.LabelOpenIDAdvancedPermsClaimDescription"></p>
<pre class="text-pre-wrap mt-2"
>{{ newAuthSettings.authOpenIDSamplePermissions }}
</pre>
</div>
</div>
</div> </div>
</transition> </transition>
</div> </div>
@ -222,6 +243,22 @@ export default {
} }
}) })
} }
function isValidClaim(claim) {
if (claim === '') return true
const pattern = new RegExp('^[a-zA-Z][a-zA-Z0-9_-]*$', 'i')
return pattern.test(claim)
}
if (!isValidClaim(this.newAuthSettings.authOpenIDGroupClaim)) {
this.$toast.error('Group Claim: Invalid claim name')
isValid = false
}
if (!isValidClaim(this.newAuthSettings.authOpenIDAdvancedPermsClaim)) {
this.$toast.error('Advanced Permission Claim: Invalid claim name')
isValid = false
}
return isValid return isValid
}, },
async saveSettings() { async saveSettings() {

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Nezahájeno", "LabelNotStarted": "Nezahájeno",
"LabelNumberOfBooks": "Počet knih", "LabelNumberOfBooks": "Počet knih",
"LabelNumberOfEpisodes": "Počet epizod", "LabelNumberOfEpisodes": "Počet epizod",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Otevřít RSS kanál", "LabelOpenRSSFeed": "Otevřít RSS kanál",
"LabelOverwrite": "Přepsat", "LabelOverwrite": "Přepsat",
"LabelPassword": "Heslo", "LabelPassword": "Heslo",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Ikke påbegyndt", "LabelNotStarted": "Ikke påbegyndt",
"LabelNumberOfBooks": "Antal bøger", "LabelNumberOfBooks": "Antal bøger",
"LabelNumberOfEpisodes": "Antal episoder", "LabelNumberOfEpisodes": "Antal episoder",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Åbn RSS-feed", "LabelOpenRSSFeed": "Åbn RSS-feed",
"LabelOverwrite": "Overskriv", "LabelOverwrite": "Overskriv",
"LabelPassword": "Kodeord", "LabelPassword": "Kodeord",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Nicht begonnen", "LabelNotStarted": "Nicht begonnen",
"LabelNumberOfBooks": "Anzahl der Hörbücher", "LabelNumberOfBooks": "Anzahl der Hörbücher",
"LabelNumberOfEpisodes": "Anzahl der Episoden", "LabelNumberOfEpisodes": "Anzahl der Episoden",
"LabelOpenIDAdvancedPermsClaimDescription": "Name des OpenID-Claims, der erweiterte Berechtigungen für Benutzeraktionen innerhalb der Anwendung enthält, die auf Nicht-Admin-Rollen angewendet werden (<b>wenn konfiguriert</b>). Wenn der Claim in der Antwort fehlt, wird der Zugang zu ABS verweigert. Fehlt eine einzelne Option, wird sie als <code>false</code> behandelt. Stelle sicher, dass der Claim des Identitätsanbieters der erwarteten Struktur entspricht:",
"LabelOpenIDClaims": "Lass die folgenden Optionen leer, um die erweiterte Zuweisung von Gruppen und Berechtigungen zu deaktivieren und automatisch die 'User'-Gruppe zuzuweisen.",
"LabelOpenIDGroupClaimDescription": "Name des OpenID-Claims, der eine Liste der Benutzergruppen enthält. Wird häufig als <code>groups</code> bezeichnet. <b>Wenn konfiguriert</b>, wird die Anwendung automatisch Rollen basierend auf den Gruppenmitgliedschaften des Benutzers zuweisen, vorausgesetzt, dass diese Gruppen im Claim als 'admin', 'user' oder 'guest' benannt sind (Groß/Kleinschreibung ist irrelevant). Der Claim eine Liste sein, und wenn ein Benutzer mehreren Gruppen angehört, wird die Anwendung die Rolle zuordnen, die dem höchsten Zugriffslevel entspricht. Wenn keine Gruppe übereinstimmt, wird der Zugang verweigert.",
"LabelOpenRSSFeed": "Öffne RSS-Feed", "LabelOpenRSSFeed": "Öffne RSS-Feed",
"LabelOverwrite": "Überschreiben", "LabelOverwrite": "Überschreiben",
"LabelPassword": "Passwort", "LabelPassword": "Passwort",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Not Started", "LabelNotStarted": "Not Started",
"LabelNumberOfBooks": "Number of Books", "LabelNumberOfBooks": "Number of Books",
"LabelNumberOfEpisodes": "# of Episodes", "LabelNumberOfEpisodes": "# of Episodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS Feed", "LabelOpenRSSFeed": "Open RSS Feed",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPassword": "Password", "LabelPassword": "Password",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Sin Iniciar", "LabelNotStarted": "Sin Iniciar",
"LabelNumberOfBooks": "Numero de Libros", "LabelNumberOfBooks": "Numero de Libros",
"LabelNumberOfEpisodes": "# de Episodios", "LabelNumberOfEpisodes": "# de Episodios",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Abrir Fuente RSS", "LabelOpenRSSFeed": "Abrir Fuente RSS",
"LabelOverwrite": "Sobrescribir", "LabelOverwrite": "Sobrescribir",
"LabelPassword": "Contraseña", "LabelPassword": "Contraseña",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Pole alustatud", "LabelNotStarted": "Pole alustatud",
"LabelNumberOfBooks": "Raamatute arv", "LabelNumberOfBooks": "Raamatute arv",
"LabelNumberOfEpisodes": "Episoodide arv", "LabelNumberOfEpisodes": "Episoodide arv",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Ava RSS voog", "LabelOpenRSSFeed": "Ava RSS voog",
"LabelOverwrite": "Kirjuta üle", "LabelOverwrite": "Kirjuta üle",
"LabelPassword": "Parool", "LabelPassword": "Parool",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Pas commencé", "LabelNotStarted": "Pas commencé",
"LabelNumberOfBooks": "Nombre de livres", "LabelNumberOfBooks": "Nombre de livres",
"LabelNumberOfEpisodes": "Nombre dépisodes", "LabelNumberOfEpisodes": "Nombre dépisodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Ouvrir le flux RSS", "LabelOpenRSSFeed": "Ouvrir le flux RSS",
"LabelOverwrite": "Écraser", "LabelOverwrite": "Écraser",
"LabelPassword": "Mot de passe", "LabelPassword": "Mot de passe",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Not Started", "LabelNotStarted": "Not Started",
"LabelNumberOfBooks": "Number of Books", "LabelNumberOfBooks": "Number of Books",
"LabelNumberOfEpisodes": "# of Episodes", "LabelNumberOfEpisodes": "# of Episodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS Feed", "LabelOpenRSSFeed": "Open RSS Feed",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPassword": "Password", "LabelPassword": "Password",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "לא התחיל", "LabelNotStarted": "לא התחיל",
"LabelNumberOfBooks": "מספר הספרים", "LabelNumberOfBooks": "מספר הספרים",
"LabelNumberOfEpisodes": "מספר הפרקים", "LabelNumberOfEpisodes": "מספר הפרקים",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "פתח ערוץ RSS", "LabelOpenRSSFeed": "פתח ערוץ RSS",
"LabelOverwrite": "לשכפל", "LabelOverwrite": "לשכפל",
"LabelPassword": "סיסמה", "LabelPassword": "סיסמה",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Not Started", "LabelNotStarted": "Not Started",
"LabelNumberOfBooks": "Number of Books", "LabelNumberOfBooks": "Number of Books",
"LabelNumberOfEpisodes": "# of Episodes", "LabelNumberOfEpisodes": "# of Episodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS Feed", "LabelOpenRSSFeed": "Open RSS Feed",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPassword": "Password", "LabelPassword": "Password",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Not Started", "LabelNotStarted": "Not Started",
"LabelNumberOfBooks": "Number of Books", "LabelNumberOfBooks": "Number of Books",
"LabelNumberOfEpisodes": "# of Episodes", "LabelNumberOfEpisodes": "# of Episodes",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Otvori RSS Feed", "LabelOpenRSSFeed": "Otvori RSS Feed",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPassword": "Lozinka", "LabelPassword": "Lozinka",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Nem indult el", "LabelNotStarted": "Nem indult el",
"LabelNumberOfBooks": "Könyvek száma", "LabelNumberOfBooks": "Könyvek száma",
"LabelNumberOfEpisodes": "Epizódok száma", "LabelNumberOfEpisodes": "Epizódok száma",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "RSS hírcsatorna megnyitása", "LabelOpenRSSFeed": "RSS hírcsatorna megnyitása",
"LabelOverwrite": "Felülírás", "LabelOverwrite": "Felülírás",
"LabelPassword": "Jelszó", "LabelPassword": "Jelszó",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Non iniziato", "LabelNotStarted": "Non iniziato",
"LabelNumberOfBooks": "Numero di libri", "LabelNumberOfBooks": "Numero di libri",
"LabelNumberOfEpisodes": "# degli episodi", "LabelNumberOfEpisodes": "# degli episodi",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Apri RSS Feed", "LabelOpenRSSFeed": "Apri RSS Feed",
"LabelOverwrite": "Sovrascrivi", "LabelOverwrite": "Sovrascrivi",
"LabelPassword": "Password", "LabelPassword": "Password",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Nepasileista", "LabelNotStarted": "Nepasileista",
"LabelNumberOfBooks": "Knygų skaičius", "LabelNumberOfBooks": "Knygų skaičius",
"LabelNumberOfEpisodes": "Epizodų skaičius", "LabelNumberOfEpisodes": "Epizodų skaičius",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Atidaryti RSS srautą", "LabelOpenRSSFeed": "Atidaryti RSS srautą",
"LabelOverwrite": "Perrašyti", "LabelOverwrite": "Perrašyti",
"LabelPassword": "Slaptažodis", "LabelPassword": "Slaptažodis",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Niet Gestart", "LabelNotStarted": "Niet Gestart",
"LabelNumberOfBooks": "Aantal Boeken", "LabelNumberOfBooks": "Aantal Boeken",
"LabelNumberOfEpisodes": "# afleveringen", "LabelNumberOfEpisodes": "# afleveringen",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Open RSS-feed", "LabelOpenRSSFeed": "Open RSS-feed",
"LabelOverwrite": "Overschrijf", "LabelOverwrite": "Overschrijf",
"LabelPassword": "Wachtwoord", "LabelPassword": "Wachtwoord",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Ikke startet", "LabelNotStarted": "Ikke startet",
"LabelNumberOfBooks": "Antall bøker", "LabelNumberOfBooks": "Antall bøker",
"LabelNumberOfEpisodes": "Antall episoder", "LabelNumberOfEpisodes": "Antall episoder",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Åpne RSS Feed", "LabelOpenRSSFeed": "Åpne RSS Feed",
"LabelOverwrite": "Overskriv", "LabelOverwrite": "Overskriv",
"LabelPassword": "Passord", "LabelPassword": "Passord",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Nie rozpoęczto", "LabelNotStarted": "Nie rozpoęczto",
"LabelNumberOfBooks": "Liczba książek", "LabelNumberOfBooks": "Liczba książek",
"LabelNumberOfEpisodes": "# odcinków", "LabelNumberOfEpisodes": "# odcinków",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Otwórz kanał RSS", "LabelOpenRSSFeed": "Otwórz kanał RSS",
"LabelOverwrite": "Overwrite", "LabelOverwrite": "Overwrite",
"LabelPassword": "Hasło", "LabelPassword": "Hasło",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Não iniciado", "LabelNotStarted": "Não iniciado",
"LabelNumberOfBooks": "Número de Livros", "LabelNumberOfBooks": "Número de Livros",
"LabelNumberOfEpisodes": "# de Episódios", "LabelNumberOfEpisodes": "# de Episódios",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Abrir Feed RSS", "LabelOpenRSSFeed": "Abrir Feed RSS",
"LabelOverwrite": "Sobrescrever", "LabelOverwrite": "Sobrescrever",
"LabelPassword": "Senha", "LabelPassword": "Senha",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Не запущено", "LabelNotStarted": "Не запущено",
"LabelNumberOfBooks": "Количество книг", "LabelNumberOfBooks": "Количество книг",
"LabelNumberOfEpisodes": "# Эпизодов", "LabelNumberOfEpisodes": "# Эпизодов",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Открыть RSS-канал", "LabelOpenRSSFeed": "Открыть RSS-канал",
"LabelOverwrite": "Перезаписать", "LabelOverwrite": "Перезаписать",
"LabelPassword": "Пароль", "LabelPassword": "Пароль",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Inte påbörjad", "LabelNotStarted": "Inte påbörjad",
"LabelNumberOfBooks": "Antal böcker", "LabelNumberOfBooks": "Antal böcker",
"LabelNumberOfEpisodes": "Antal avsnitt", "LabelNumberOfEpisodes": "Antal avsnitt",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Öppna RSS-flöde", "LabelOpenRSSFeed": "Öppna RSS-flöde",
"LabelOverwrite": "Skriv över", "LabelOverwrite": "Skriv över",
"LabelPassword": "Lösenord", "LabelPassword": "Lösenord",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "Chưa bắt đầu", "LabelNotStarted": "Chưa bắt đầu",
"LabelNumberOfBooks": "Số lượng Sách", "LabelNumberOfBooks": "Số lượng Sách",
"LabelNumberOfEpisodes": "# của Tập", "LabelNumberOfEpisodes": "# của Tập",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "Mở RSS Feed", "LabelOpenRSSFeed": "Mở RSS Feed",
"LabelOverwrite": "Ghi đè", "LabelOverwrite": "Ghi đè",
"LabelPassword": "Mật khẩu", "LabelPassword": "Mật khẩu",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "未开始", "LabelNotStarted": "未开始",
"LabelNumberOfBooks": "图书数量", "LabelNumberOfBooks": "图书数量",
"LabelNumberOfEpisodes": "# 集", "LabelNumberOfEpisodes": "# 集",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "打开 RSS 源", "LabelOpenRSSFeed": "打开 RSS 源",
"LabelOverwrite": "覆盖", "LabelOverwrite": "覆盖",
"LabelPassword": "密码", "LabelPassword": "密码",

View File

@ -385,6 +385,9 @@
"LabelNotStarted": "未開始", "LabelNotStarted": "未開始",
"LabelNumberOfBooks": "圖書數量", "LabelNumberOfBooks": "圖書數量",
"LabelNumberOfEpisodes": "# 集", "LabelNumberOfEpisodes": "# 集",
"LabelOpenIDAdvancedPermsClaimDescription": "Name of the OpenID claim that contains advanced permissions for user actions within the application which will apply to non-admin roles (<b>if configured</b>). If the claim is missing from the response, access to ABS will be denied. If a single option is missing, it will be treated as <code>false</code>. Ensure the identity provider's claim matches the expected structure:",
"LabelOpenIDClaims": "Leave the following options empty to disable advanced group and permissions assignment, automatically assigning 'User' group then.",
"LabelOpenIDGroupClaimDescription": "Name of the OpenID claim that contains a list of the user's groups. Commonly referred to as <code>groups</code>. <b>If configured</b>, the application will automatically assign roles based on the user's group memberships, provided that these groups are named case-insensitively 'admin', 'user', or 'guest' in the claim. The claim should contain a list, and if a user belongs to multiple groups, the application will assign the role corresponding to the highest level of access. If no group matches, access will be denied.",
"LabelOpenRSSFeed": "打開 RSS 源", "LabelOpenRSSFeed": "打開 RSS 源",
"LabelOverwrite": "覆蓋", "LabelOverwrite": "覆蓋",
"LabelPassword": "密碼", "LabelPassword": "密碼",

View File

@ -98,73 +98,200 @@ class Auth {
scope: 'openid profile email' scope: 'openid profile email'
} }
}, async (tokenset, userinfo, done) => { }, async (tokenset, userinfo, done) => {
Logger.debug(`[Auth] openid callback userinfo=`, userinfo) try {
Logger.debug(`[Auth] openid callback userinfo=`, JSON.stringify(userinfo, null, 2))
let failureMessage = 'Unauthorized'
if (!userinfo.sub) { if (!userinfo.sub) {
Logger.error(`[Auth] openid callback invalid userinfo, no sub`) throw new Error('Invalid userinfo, no sub')
return done(null, null, failureMessage)
} }
// First check for matching user by sub if (!this.validateGroupClaim(userinfo)) {
let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub) throw new Error(`Group claim ${Database.serverSettings.authOpenIDGroupClaim} not found or empty in userinfo`)
if (!user) {
// Optionally match existing by email or username based on server setting "authOpenIDMatchExistingBy"
if (Database.serverSettings.authOpenIDMatchExistingBy === 'email' && userinfo.email && userinfo.email_verified) {
Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`)
user = await Database.userModel.getUserByEmail(userinfo.email)
// Check that user is not already matched
if (user?.authOpenIDSub) {
Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`)
// TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback
failureMessage = 'A matching user was found but is already matched with another user from your auth provider'
user = null
}
} else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username' && userinfo.preferred_username) {
Logger.info(`[Auth] openid: User not found, checking existing with username "${userinfo.preferred_username}"`)
user = await Database.userModel.getUserByUsername(userinfo.preferred_username)
// Check that user is not already matched
if (user?.authOpenIDSub) {
Logger.warn(`[Auth] openid: User found with username "${userinfo.preferred_username}" but is already matched with sub "${user.authOpenIDSub}"`)
// TODO: Message isn't actually returned to the user yet. Need to override the passport authenticated callback
failureMessage = 'A matching user was found but is already matched with another user from your auth provider'
user = null
}
} }
// If existing user was matched and isActive then save sub to user let user = await this.findOrCreateUser(userinfo)
if (user?.isActive) {
Logger.info(`[Auth] openid: New user found matching existing user "${user.username}"`)
user.authOpenIDSub = userinfo.sub
await Database.userModel.updateFromOld(user)
} else if (user && !user.isActive) {
Logger.warn(`[Auth] openid: New user found matching existing user "${user.username}" but that user is deactivated`)
}
// Optionally auto register the user
if (!user && Database.serverSettings.authOpenIDAutoRegister) {
Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this)
}
}
if (!user?.isActive) { if (!user?.isActive) {
if (user && !user.isActive) { throw new Error('User not active or not found')
failureMessage = 'Unauthorized'
}
// deny login
done(null, null, failureMessage)
return
} }
await this.setUserGroup(user, userinfo)
await this.updateUserPermissions(user, userinfo)
// We also have to save the id_token for later (used for logout) because we cannot set cookies here // We also have to save the id_token for later (used for logout) because we cannot set cookies here
user.openid_id_token = tokenset.id_token user.openid_id_token = tokenset.id_token
// permit login
return done(null, user) return done(null, user)
} catch (error) {
Logger.error(`[Auth] openid callback error: ${error?.message}\n${error?.stack}`)
return done(null, null, 'Unauthorized')
}
})) }))
} }
/**
* Finds an existing user by OpenID subject identifier, or by email/username based on server settings,
* or creates a new user if configured to do so.
*/
async findOrCreateUser(userinfo) {
let user = await Database.userModel.getUserByOpenIDSub(userinfo.sub)
// Matched by sub
if (user) {
Logger.debug(`[Auth] openid: User found by sub`)
return user
}
// Match existing user by email
if (Database.serverSettings.authOpenIDMatchExistingBy === 'email') {
if (userinfo.email) {
// Only disallow when email_verified explicitly set to false (allow both if not set or true)
if (userinfo.email_verified === false) {
Logger.warn(`[Auth] openid: User not found and email "${userinfo.email}" is not verified`)
return null
} else {
Logger.info(`[Auth] openid: User not found, checking existing with email "${userinfo.email}"`)
user = await Database.userModel.getUserByEmail(userinfo.email)
if (user?.authOpenIDSub) {
Logger.warn(`[Auth] openid: User found with email "${userinfo.email}" but is already matched with sub "${user.authOpenIDSub}"`)
return null // User is linked to a different OpenID subject; do not proceed.
}
}
} else {
Logger.warn(`[Auth] openid: User not found and no email in userinfo`)
// We deny login, because if the admin whishes to match email, it makes sense to require it
return null
}
}
// Match existing user by username
else if (Database.serverSettings.authOpenIDMatchExistingBy === 'username') {
let username
if (userinfo.preferred_username) {
Logger.info(`[Auth] openid: User not found, checking existing with userinfo.preferred_username "${userinfo.preferred_username}"`)
username = userinfo.preferred_username
} else if (userinfo.username) {
Logger.info(`[Auth] openid: User not found, checking existing with userinfo.username "${userinfo.username}"`)
username = userinfo.username
} else {
Logger.warn(`[Auth] openid: User not found and neither preferred_username nor username in userinfo`)
return null
}
user = await Database.userModel.getUserByUsername(username)
if (user?.authOpenIDSub) {
Logger.warn(`[Auth] openid: User found with username "${username}" but is already matched with sub "${user.authOpenIDSub}"`)
return null // User is linked to a different OpenID subject; do not proceed.
}
}
// Found existing user via email or username
if (user) {
if (!user.isActive) {
Logger.warn(`[Auth] openid: User found but is not active`)
return null
}
user.authOpenIDSub = userinfo.sub
await Database.userModel.updateFromOld(user)
Logger.debug(`[Auth] openid: User found by email/username`)
return user
}
// If no existing user was matched, auto-register if configured
if (Database.serverSettings.authOpenIDAutoRegister) {
Logger.info(`[Auth] openid: Auto-registering user with sub "${userinfo.sub}"`, userinfo)
user = await Database.userModel.createUserFromOpenIdUserInfo(userinfo, this)
return user
}
Logger.warn(`[Auth] openid: User not found and auto-register is disabled`)
return null
}
/**
* Validates the presence and content of the group claim in userinfo.
*/
validateGroupClaim(userinfo) {
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
if (!groupClaimName) // Allow no group claim when configured like this
return true
// If configured it must exist in userinfo
if (!userinfo[groupClaimName]) {
return false
}
return true
}
/**
* Sets the user group based on group claim in userinfo.
*
* @param {import('./objects/user/User')} user
* @param {Object} userinfo
*/
async setUserGroup(user, userinfo) {
const groupClaimName = Database.serverSettings.authOpenIDGroupClaim
if (!groupClaimName) // No group claim configured, don't set anything
return
if (!userinfo[groupClaimName])
throw new Error(`Group claim ${groupClaimName} not found in userinfo`)
const groupsList = userinfo[groupClaimName].map(group => group.toLowerCase())
const rolesInOrderOfPriority = ['admin', 'user', 'guest']
let userType = rolesInOrderOfPriority.find(role => groupsList.includes(role))
if (userType) {
if (user.type === 'root') {
// Check OpenID Group
if (userType !== 'admin') {
throw new Error(`Root user "${user.username}" cannot be downgraded to ${userType}. Denying login.`)
} else {
// If root user is logging in via OpenID, we will not change the type
return
}
}
if (user.type !== userType) {
Logger.info(`[Auth] openid callback: Updating user "${user.username}" type to "${userType}" from "${user.type}"`)
user.type = userType
await Database.userModel.updateFromOld(user)
}
} else {
throw new Error(`No valid group found in userinfo: ${JSON.stringify(userinfo[groupClaimName], null, 2)}`)
}
}
/**
* Updates user permissions based on the advanced permissions claim.
*
* @param {import('./objects/user/User')} user
* @param {Object} userinfo
*/
async updateUserPermissions(user, userinfo) {
const absPermissionsClaim = Database.serverSettings.authOpenIDAdvancedPermsClaim
if (!absPermissionsClaim) // No advanced permissions claim configured, don't set anything
return
if (user.type === 'admin' || user.type === 'root')
return
const absPermissions = userinfo[absPermissionsClaim]
if (!absPermissions)
throw new Error(`Advanced permissions claim ${absPermissionsClaim} not found in userinfo`)
if (user.updatePermissionsFromExternalJSON(absPermissions)) {
Logger.info(`[Auth] openid callback: Updating advanced perms for user "${user.username}" using "${JSON.stringify(absPermissions)}"`)
await Database.userModel.updateFromOld(user)
}
}
/** /**
* Unuse strategy * Unuse strategy
* *
@ -334,10 +461,19 @@ class Auth {
sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback sso_redirect_uri: oidcStrategy._params.redirect_uri // Save the redirect_uri (for the SSO Provider) for the callback
} }
var scope = 'openid profile email'
if (global.ServerSettings.authOpenIDGroupClaim) {
scope += ' ' + global.ServerSettings.authOpenIDGroupClaim
}
if (global.ServerSettings.authOpenIDAdvancedPermsClaim) {
scope += ' ' + global.ServerSettings.authOpenIDAdvancedPermsClaim
}
const authorizationUrl = client.authorizationUrl({ const authorizationUrl = client.authorizationUrl({
...oidcStrategy._params, ...oidcStrategy._params,
state: state, state: state,
response_type: 'code', response_type: 'code',
scope: scope,
code_challenge, code_challenge,
code_challenge_method code_challenge_method
}) })
@ -346,7 +482,7 @@ class Auth {
res.redirect(authorizationUrl) res.redirect(authorizationUrl)
} catch (error) { } catch (error) {
Logger.error(`[Auth] Error in /auth/openid route: ${error}`) Logger.error(`[Auth] Error in /auth/openid route: ${error}\n${error?.stack}`)
res.status(500).send('Internal Server Error') res.status(500).send('Internal Server Error')
} }
@ -402,7 +538,7 @@ class Auth {
// Redirect to the overwrite URI saved in the map // Redirect to the overwrite URI saved in the map
res.redirect(redirectUri) res.redirect(redirectUri)
} catch (error) { } catch (error) {
Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}`) Logger.error(`[Auth] Error in /auth/openid/mobile-redirect route: ${error}\n${error?.stack}`)
res.status(500).send('Internal Server Error') res.status(500).send('Internal Server Error')
} }
}) })
@ -424,12 +560,12 @@ class Auth {
} }
function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) { function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) {
Logger.error(logMessage) Logger.error(JSON.stringify(logMessage, null, 2))
if (response) { if (response) {
// Depending on the error, it can also have a body // Depending on the error, it can also have a body
// We also log the request header the passport plugin sents for the URL // We also log the request header the passport plugin sents for the URL
const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED') const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED')
Logger.debug(header + '\n' + response.body?.toString() + '\n' + JSON.stringify(response.body, null, 2)) Logger.debug(header + '\n' + JSON.stringify(response.body, null, 2))
} }
if (isMobile) { if (isMobile) {

View File

@ -1,6 +1,7 @@
const packageJson = require('../../../package.json') const packageJson = require('../../../package.json')
const { BookshelfView } = require('../../utils/constants') const { BookshelfView } = require('../../utils/constants')
const Logger = require('../../Logger') const Logger = require('../../Logger')
const User = require('../user/User')
class ServerSettings { class ServerSettings {
constructor(settings) { constructor(settings) {
@ -72,6 +73,8 @@ class ServerSettings {
this.authOpenIDAutoRegister = false this.authOpenIDAutoRegister = false
this.authOpenIDMatchExistingBy = null this.authOpenIDMatchExistingBy = null
this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth'] this.authOpenIDMobileRedirectURIs = ['audiobookshelf://oauth']
this.authOpenIDGroupClaim = ''
this.authOpenIDAdvancedPermsClaim = ''
if (settings) { if (settings) {
this.construct(settings) this.construct(settings)
@ -129,6 +132,8 @@ class ServerSettings {
this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister this.authOpenIDAutoRegister = !!settings.authOpenIDAutoRegister
this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null this.authOpenIDMatchExistingBy = settings.authOpenIDMatchExistingBy || null
this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth'] this.authOpenIDMobileRedirectURIs = settings.authOpenIDMobileRedirectURIs || ['audiobookshelf://oauth']
this.authOpenIDGroupClaim = settings.authOpenIDGroupClaim || ''
this.authOpenIDAdvancedPermsClaim = settings.authOpenIDAdvancedPermsClaim || ''
if (!Array.isArray(this.authActiveAuthMethods)) { if (!Array.isArray(this.authActiveAuthMethods)) {
this.authActiveAuthMethods = ['local'] this.authActiveAuthMethods = ['local']
@ -216,7 +221,9 @@ class ServerSettings {
authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
authOpenIDAutoRegister: this.authOpenIDAutoRegister, authOpenIDAutoRegister: this.authOpenIDAutoRegister,
authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client
authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client
authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim // Do not return to client
} }
} }
@ -226,6 +233,8 @@ class ServerSettings {
delete json.authOpenIDClientID delete json.authOpenIDClientID
delete json.authOpenIDClientSecret delete json.authOpenIDClientSecret
delete json.authOpenIDMobileRedirectURIs delete json.authOpenIDMobileRedirectURIs
delete json.authOpenIDGroupClaim
delete json.authOpenIDAdvancedPermsClaim
return json return json
} }
@ -262,7 +271,11 @@ class ServerSettings {
authOpenIDAutoLaunch: this.authOpenIDAutoLaunch, authOpenIDAutoLaunch: this.authOpenIDAutoLaunch,
authOpenIDAutoRegister: this.authOpenIDAutoRegister, authOpenIDAutoRegister: this.authOpenIDAutoRegister,
authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy, authOpenIDMatchExistingBy: this.authOpenIDMatchExistingBy,
authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs // Do not return to client authOpenIDMobileRedirectURIs: this.authOpenIDMobileRedirectURIs, // Do not return to client
authOpenIDGroupClaim: this.authOpenIDGroupClaim, // Do not return to client
authOpenIDAdvancedPermsClaim: this.authOpenIDAdvancedPermsClaim, // Do not return to client
authOpenIDSamplePermissions: User.getSampleAbsPermissions()
} }
} }

View File

@ -268,6 +268,111 @@ class User {
return hasUpdates return hasUpdates
} }
// List of expected permission properties from the client
static permissionMapping = {
canDownload: 'download',
canUpload: 'upload',
canDelete: 'delete',
canUpdate: 'update',
canAccessExplicitContent: 'accessExplicitContent',
canAccessAllLibraries: 'accessAllLibraries',
canAccessAllTags: 'accessAllTags',
tagsAreDenylist: 'selectedTagsNotAccessible',
// Direct mapping for array-based permissions
allowedLibraries: 'librariesAccessible',
allowedTags: 'itemTagsSelected'
}
/**
* Update user permissions from external JSON
*
* @param {Object} absPermissions JSON containing user permissions
* @returns {boolean} true if updates were made
*/
updatePermissionsFromExternalJSON(absPermissions) {
let hasUpdates = false
let updatedUserPermissions = {}
// Initialize all permissions to false first
Object.keys(User.permissionMapping).forEach(mappingKey => {
const userPermKey = User.permissionMapping[mappingKey]
if (typeof this.permissions[userPermKey] === 'boolean') {
updatedUserPermissions[userPermKey] = false // Default to false for boolean permissions
}
})
// Map the boolean permissions from absPermissions
Object.keys(absPermissions).forEach(absKey => {
const userPermKey = User.permissionMapping[absKey]
if (!userPermKey) {
throw new Error(`Unexpected permission property: ${absKey}`)
}
if (updatedUserPermissions[userPermKey] !== undefined) {
updatedUserPermissions[userPermKey] = !!absPermissions[absKey]
}
})
// Update user permissions if changes were made
if (JSON.stringify(this.permissions) !== JSON.stringify(updatedUserPermissions)) {
this.permissions = updatedUserPermissions
hasUpdates = true
}
// Handle allowedLibraries
if (this.permissions.accessAllLibraries) {
if (this.librariesAccessible.length) {
this.librariesAccessible = []
hasUpdates = true
}
} else if (absPermissions.allowedLibraries?.length && absPermissions.allowedLibraries.join(',') !== this.librariesAccessible.join(',')) {
if (absPermissions.allowedLibraries.some(lid => typeof lid !== 'string')) {
throw new Error('Invalid permission property "allowedLibraries", expecting array of strings')
}
this.librariesAccessible = absPermissions.allowedLibraries
hasUpdates = true
}
// Handle allowedTags
if (this.permissions.accessAllTags) {
if (this.itemTagsSelected.length) {
this.itemTagsSelected = []
hasUpdates = true
}
} else if (absPermissions.allowedTags?.length && absPermissions.allowedTags.join(',') !== this.itemTagsSelected.join(',')) {
if (absPermissions.allowedTags.some(tag => typeof tag !== 'string')) {
throw new Error('Invalid permission property "allowedTags", expecting array of strings')
}
this.itemTagsSelected = absPermissions.allowedTags
hasUpdates = true
}
return hasUpdates
}
/**
* Get a sample to show how a JSON for updatePermissionsFromExternalJSON should look like
*
* @returns {string} JSON string
*/
static getSampleAbsPermissions() {
// Start with a template object where all permissions are false for simplicity
const samplePermissions = Object.keys(User.permissionMapping).reduce((acc, key) => {
// For array-based permissions, provide a sample array
if (key === 'allowedLibraries') {
acc[key] = [`5406ba8a-16e1-451d-96d7-4931b0a0d966`, `918fd848-7c1d-4a02-818a-847435a879ca`]
} else if (key === 'allowedTags') {
acc[key] = [`ExampleTag`, `AnotherTag`, `ThirdTag`]
} else {
acc[key] = false
}
return acc
}, {})
return JSON.stringify(samplePermissions, null, 2) // Pretty print the JSON
}
/** /**
* Get first available library id for user * Get first available library id for user
* *