adjusted SSOSettings model and changed all code based on the model

This commit is contained in:
David Leimroth 2022-02-10 14:45:21 +01:00
parent b12a38d12b
commit 3cd09eecba
9 changed files with 251 additions and 103 deletions

View File

@ -112,6 +112,9 @@ export default {
if (payload.serverSettings) { if (payload.serverSettings) {
this.$store.commit('setServerSettings', payload.serverSettings) this.$store.commit('setServerSettings', payload.serverSettings)
} }
if (payload.SSOSettings) {
this.$store.commit('sso/setSSOSettings', payload.SSOSettings)
}
// Start scans currently running // Start scans currently running
if (payload.librariesScanning) { if (payload.librariesScanning) {

View File

@ -9,32 +9,32 @@
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p class="pl-4 text-lg">Issuer&nbsp;</p> <p class="pl-4 text-lg">Issuer&nbsp;</p>
<ui-text-input v-model="oidc.issuer" :disabled="updatingSSOSettings" @input="updateSSOIssuer" /> <ui-text-input v-model="issuer" :disabled="updatingSSOSettings" />
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p class="pl-4 text-lg">Authorization URL&nbsp;</p> <p class="pl-4 text-lg">Authorization URL&nbsp;</p>
<ui-text-input v-model="oidc.authorizationURL" :disabled="updatingSSOSettings" @input="updateAuthorizationURL" /> <ui-text-input v-model="authorizationURL" :disabled="updatingSSOSettings" />
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p class="pl-4 text-lg">Token URL&nbsp;</p> <p class="pl-4 text-lg">Token URL&nbsp;</p>
<ui-text-input v-model="oidc.tokenURL" :disabled="updatingSSOSettings" @input="updateTokenURL" /> <ui-text-input v-model="tokenURL" :disabled="updatingSSOSettings" />
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p class="pl-4 text-lg">User Info URL&nbsp;</p> <p class="pl-4 text-lg">User Info URL&nbsp;</p>
<ui-text-input v-model="oidc.userInfoURL" :disabled="updatingSSOSettings" @input="updateUserInfoURL" /> <ui-text-input v-model="userInfoURL" :disabled="updatingSSOSettings" />
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p class="pl-4 text-lg">Client ID&nbsp;</p> <p class="pl-4 text-lg">Client ID&nbsp;</p>
<ui-text-input v-model="oidc.clientID" :disabled="updatingSSOSettings" @input="updateClientID" /> <ui-text-input v-model="clientID" :disabled="updatingSSOSettings" />
</div> </div>
<div class="flex items-center py-2"> <div class="flex items-center py-2">
<p class="pl-4 text-lg">Client Secret&nbsp;</p> <p class="pl-4 text-lg">Client Secret&nbsp;</p>
<ui-text-input type="password" v-model="oidc.clientSecret" :disabled="updatingSSOSettings" @input="updateClientSecret" /> <ui-text-input type="password" v-model="clientSecret" :disabled="updatingSSOSettings" />
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
@ -42,32 +42,32 @@
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<ui-toggle-switch v-model="permissions.createNewUser" :disabled="updatingSSOSettings" @input="updatePermissionCreateNewUser" /> <ui-toggle-switch v-model="createNewUser" :disabled="updatingSSOSettings" />
<p class="pl-4 text-lg">Create a new user on first login</p> <p class="pl-4 text-lg">Create a new user on first login</p>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<ui-toggle-switch v-model="permissions.download" :disabled="updatingSSOSettings || !permissions.createNewUser" @input="updatePermissionDownload" /> <ui-toggle-switch v-model="permissionDownload" :disabled="updatingSSOSettings || !createNewUser" />
<p class="pl-4 text-lg">The new user is allowed to download</p> <p class="pl-4 text-lg">The new user is allowed to download</p>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<ui-toggle-switch v-model="permissions.update" :disabled="updatingSSOSettings || !permissions.createNewUser" @input="updatePermissionUpdate" /> <ui-toggle-switch v-model="permissionUpdate" :disabled="updatingSSOSettings || !createNewUser" />
<p class="pl-4 text-lg">The new user is allowed to update</p> <p class="pl-4 text-lg">The new user is allowed to update</p>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<ui-toggle-switch v-model="permissions.delete" :disabled="updatingSSOSettings || !permissions.createNewUser" @input="updatePermissionDelete" /> <ui-toggle-switch v-model="permissionDelete" :disabled="updatingSSOSettings || !createNewUser" />
<p class="pl-4 text-lg">The new user is allowed to delete</p> <p class="pl-4 text-lg">The new user is allowed to delete</p>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<ui-toggle-switch v-model="permissions.upload" :disabled="updatingSSOSettings || !permissions.createNewUser" @input="updatePermissionUpload" /> <ui-toggle-switch v-model="permissionUpload" :disabled="updatingSSOSettings || !createNewUser" />
<p class="pl-4 text-lg">The new user is allowed to upload</p> <p class="pl-4 text-lg">The new user is allowed to upload</p>
</div> </div>
<div class="flex items-center mb-2"> <div class="flex items-center mb-2">
<ui-toggle-switch v-model="permissions.accessAllLibraries" :disabled="updatingSSOSettings || !permissions.createNewUser" @input="updatePermissionAccessAllLibraries" /> <ui-toggle-switch v-model="permissionAccessAllLibraries" :disabled="updatingSSOSettings || !createNewUser" />
<p class="pl-4 text-lg">The new user is allowed to access all libraries</p> <p class="pl-4 text-lg">The new user is allowed to access all libraries</p>
</div> </div>
@ -90,16 +90,28 @@ export default {
clientID: "", clientID: "",
clientSecret: "", clientSecret: "",
}, },
user: {
permissions: {
createNewUser: false, createNewUser: false,
download: false, isActive: true,
update: false, settings: {
delete: false, mobileOrderBy: 'recent',
upload: false, mobileOrderDesc: true,
accessAllLibraries: false mobileFilterBy: 'all',
orderBy: 'book.title',
orderDesc: false,
filterBy: 'all',
playbackRate: 1,
bookshelfCoverSize: 120,
collapseSeries: false
},
permissions: {
download: false,
update: false,
delete: false,
upload: false,
accessAllLibraries: false
}
}, },
updatingSSOSettings: false, updatingSSOSettings: false,
newSSOSettings: {} newSSOSettings: {}
} }
@ -114,55 +126,136 @@ export default {
}, },
computed: { computed: {
SSOSettings() { SSOSettings() {
return this.$store.state.SSOSettings return this.$store.state.sso
},
issuer: {
get() {
return this.oidc.issuer
return this.$store.state.sso.oidc.issuer
},
set(val) {
this.oidc.issuer = val
// this.$store.state.sso.oidc.issuer = val
}
},
authorizationURL: {
get() {
return this.oidc.authorizationURL
return this.$store.state.sso.oidc.authorizationURL
},
set(val) {
this.oidc.authorizationURL = val
// this.$store.state.sso.oidc.authorizationURL = val
}
},
tokenURL: {
get() {
return this.oidc.tokenURL
return this.$store.state.sso.oidc.tokenURL
},
set(val) {
this.oidc.tokenURL = val
// this.$store.state.sso.oidc.tokenURL = val
}
},
userInfoURL: {
get() {
return this.oidc.userInfoURL
return this.$store.state.sso.oidc.userInfoURL
},
set(val) {
this.oidc.userInfoURL = val
// this.$store.state.sso.oidc.userInfoURL = val
},
},
clientID: {
get() {
return this.oidc.clientID
return this.$store.state.sso.oidc.clientID
},
set(val) {
this.oidc.clientID = val
// this.$store.state.sso.oidc.clientID = val
},
},
clientSecret: {
get() {
return this.oidc.clientSecret
return this.$store.state.sso.oidc.clientSecret
},
set(val) {
this.oidc.clientSecret = val
// this.$store.state.sso.oidc.clientSecret = val
},
},
createNewUser: {
get() {
return this.user.createNewUser
return this.$store.state.sso.createNewUser
},
set(val) {
this.user.createNewUser = val
// this.$store.state.sso.createNewUser = val
},
},
permissionDownload: {
get() {
return this.user.permissions.download
return this.$store.state.sso.permissions.download
},
set(val) {
this.user.permissions.download = val
// this.$store.state.sso.permissions.download = val
},
},
permissionUpdate: {
get() {
return this.user.permissions.update
return this.$store.state.sso.permissions.update
},
set(val) {
this.user.permissions.update = val
// this.$store.state.sso.permissions.update = val
},
},
permissionDelete: {
get() {
return this.user.permissions.delete
return this.$store.state.sso.permissions.delete
},
set(val) {
this.user.permissions.delete = val
// this.$store.state.sso.permissions.delete = val
},
},
permissionUpload: {
get() {
return this.user.permissions.upload
return this.$store.state.sso.permissions.upload
},
set(val) {
this.user.permissions.upload = val
// this.$store.state.sso.permissions.upload = val
}
},
permissionAccessAllLibraries: {
get() {
return this.user.permissions.accessAllLibraries
return this.$store.state.sso.permissions.accessAllLibraries
},
set(val) {
this.user.permissions.accessAllLibraries = val
// this.$store.state.sso.permissions.accessAllLibraries = val
}
}, },
}, },
methods: { methods: {
updateSSOIssuer(val) {
this.oidc.issuer = val
},
updateAuthorizationURL(val) {
this.oidc.authorizationURL = val
},
updateTokenURL(val) {
this.oidc.tokenURL = val
},
updateUserInfoURL(val) {
this.oidc.userInfoURL = val
},
updateClientID(val) {
this.oidc.clientID = val
},
updateClientSecret(val) {
this.oidc.clientSecret = val
},
updatePermissionCreateNewUser(val) {
this.permissions.createNewUser = val
},
updatePermissionDownload(val) {
this.permissions.createNewUser = val
},
updatePermissionUpdate(val) {
this.permissions.createNewUser = val
},
updatePermissionDelete(val) {
this.permissions.createNewUser = val
},
updatePermissionUpload(val) {
this.permissions.createNewUser = val
},
updatePermissionAccessAllLibraries(val) {
this.permissions.createNewUser = val
},
updatePermissionCreateNewUser(val) {
this.permissions.createNewUser = val
},
saveSSOSettings(payload) { saveSSOSettings(payload) {
this.updatingSSOSettings = true this.updatingSSOSettings = true
this.$store this.$store
.dispatch('sso/updateSSOSettings', {oidc: this.oidc, permissions: this.permissions}) .dispatch('sso/updateSSOSettings', {oidc: this.oidc, user: this.user})
.then((success) => { .then((success) => {
console.log('Updated SSO Settings', success)
this.updatingSSOSettings = false this.updatingSSOSettings = false
}) })
.catch((error) => { .catch((error) => {
@ -171,20 +264,21 @@ export default {
}) })
}, },
initSSOSettings() { initSSOSettings() {
this.newSSOSettings = this.SSOSettings ? { ...this.SSOSettings } : {} for (const key in this.$store.state.sso.oidc) {
this.oidc[key] = this.$store.state.sso.oidc[key]
}
for (const key in this.$store.state.sso.user) {
this.oidc.issuer = this.newSSOSettings.issuer if (typeof this.$store.state.sso.user[key] === "object" && typeof this.user[key] === "object") {
this.oidc.authorizationURL = this.newSSOSettings.authorizationURL for (const key2 in this.$store.state.sso.user[key]) {
this.oidc.tokenURL = this.newSSOSettings.tokenURL this.user[key][key2] = this.$store.state.sso.user[key][key2]
this.oidc.userInfoURL = this.newSSOSettings.userInfoURL }
this.oidc.clientID = this.newSSOSettings.clientID continue
this.oidc.clientSecret = this.newSSOSettings.clientSecret }
this.updatingSSOSettings = this.newSSOSettings.updatingSSOSettings if (this.user[key] !== undefined) {
this.user[key] = this.$store.state.sso.user[key]
// this.newSSOSettings.coverDestination === this.$constants.CoverDestination.AUDIOBOOK }
// this.useSquareBookCovers = this.newSSOSettings.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE }
// this.useAlternativeBookshelfView = this.newSSOSettings.bookshelfView === this.$constants.BookshelfView.TITLES
}, },
}, },

View File

@ -111,7 +111,6 @@ export default {
this.$axios this.$axios
.$post('/api/authorize', null, {}) .$post('/api/authorize', null, {})
.then((res) => { .then((res) => {
console.log({res})
this.setUser(res.user) this.setUser(res.user)
this.processing = false this.processing = false
}) })

View File

@ -87,20 +87,21 @@ export const actions = {
export const mutations = { export const mutations = {
setSSOSettings(state, settings) { setSSOSettings(state, settings) {
if (!settings) return if (!settings || !settings.oidc || !settings.user) return
for (const key in settings) { for (const key in settings.oidc) {
if (state.oidc[key] !== settings[key]) { state.oidc[key] = settings.oidc[key]
state.oidc[key] = settings[key]
}
} }
},
setUserPermissions(state, permissions) {
if (!permissions) return
for (const key in permissions) { for (const key in settings.user) {
if (state.user.permissions[key] !== permissions[key]) { if (typeof settings.user[key] === "object" && typeof state.user[key] === "object") {
state.user.permissions[key] = permissions[key] for (const key2 in settings.user[key]) {
state.user[key][key2] = settings.user[key][key2]
}
continue
}
if (state.user[key] !== undefined) {
state.user[key] = settings.user[key]
} }
} }
}, },

View File

@ -297,18 +297,20 @@ class ApiController {
return res.sendStatus(403) return res.sendStatus(403)
} }
let SSOUpdate = req.body let SSOUpdate = req.body
Logger.debug("SSOUpdate=", SSOUpdate)
if (!SSOUpdate || !isObject(SSOUpdate)) { if (!SSOUpdate || !isObject(SSOUpdate)) {
return res.status(500).send('Invalid settings update object') return res.status(500).send('Invalid settings update object')
} }
console.log("SSOUpdate", JSON.stringify(SSOUpdate))
var madeUpdates = this.db.SSOSettings.update(SSOUpdate) var madeUpdates = this.db.SSOSettings.update(SSOUpdate)
console.log("SSOUpdate", JSON.stringify(this.db.SSOSettings))
if (madeUpdates) { if (madeUpdates) {
await this.db.updateEntity('sso', this.db.SSOUpdate) await this.db.updateEntity('settings', this.db.SSOSettings)
} }
return res.json({ return res.json({
success: true, success: true,
SSOUpdate: this.db.SSOUpdate SSOUpdate: this.db.SSOSettings
}) })
} }

View File

@ -217,11 +217,12 @@ class Auth {
async handleOIDCVerification(issuer, profile, cb) { async handleOIDCVerification(issuer, profile, cb) {
Logger.debug(`[Auth] handleOIDCVerification ${issuer}`) Logger.debug(`[Auth] handleOIDCVerification ${issuer}`)
let user = this.db.users.find(u => u.id === profile.id) let user = this.db.users.find(u => u.ssoId === profile.id)
if (!user && this.db.SSOSettings.createNewUser) { if (!user && this.db.SSOSettings.user.createNewUser) {
// create a user // create a user
let account = {} let account = {}
account.id = profile.id account.id = profile.username
account.ssoId = profile.id
account.username = profile.username account.username = profile.username
account.isActive = true account.isActive = true
account.type = "guest" account.type = "guest"

View File

@ -85,6 +85,9 @@ class Server {
return client.user.toJSONForPublic(this.streamManager.streams) return client.user.toJSONForPublic(this.streamManager.streams)
}) })
} }
get SSOSettings() {
return this.db.SSOSettings
}
getClientsForUser(userId) { getClientsForUser(userId) {
return Object.values(this.clients).filter(c => c.user && c.user.id === userId) return Object.values(this.clients).filter(c => c.user && c.user.id === userId)
@ -679,6 +682,7 @@ class Server {
} }
if (user.type === 'root') { if (user.type === 'root') {
initialPayload.usersOnline = this.usersOnline initialPayload.usersOnline = this.usersOnline
initialPayload.SSOSettings = this.SSOSettings
} }
client.socket.emit('init', initialPayload) client.socket.emit('init', initialPayload)

View File

@ -1,22 +1,51 @@
const Logger = require('../Logger') const Logger = require('../Logger')
const User = require('./User') const User = require('./User')
const { isObject } = require('../utils') const { isObject } = require('../utils')
const { difference } = require('../utils/string')
const defaultSettings = { const defaultSettings = {
createNewUser: false, createNewUser: false,
userPermissions: User.getDefaultUserPermissions('guest') user: {
createNewUser: false,
isActive: true,
settings: {
mobileOrderBy: 'recent',
mobileOrderDesc: true,
mobileFilterBy: 'all',
orderBy: 'book.title',
orderDesc: false,
filterBy: 'all',
playbackRate: 1,
bookshelfCoverSize: 120,
collapseSeries: false
},
permissions: User.getDefaultUserPermissions('guest')
}
} }
class SSOSettings { class SSOSettings {
constructor(settings = defaultSettings) { constructor(settings = defaultSettings) {
this.id = 'sso-settings' this.id = 'sso-settings'
this.createNewUser = !!settings.createNewUser this.user = { ...settings.user }
this.userPermissions = { ...settings.userPermissions } this.initOIDCSettings(settings);
this.initOIDCSettings(); Logger.debug("[SSOSettings.constructor]", this.toJSON)
} }
initOIDCSettings() { initOIDCSettings(settings) {
// can't be part of default settings, because apperently process.env is not set in the beginning // can't be part of default settings, because apperently process.env is not set in the beginning
if (settings && settings.oidc) {
this.oidc = {
issuer: settings.oidc.issuer,
authorizationURL: settings.oidc.authorizationURL,
tokenURL: settings.oidc.tokenURL,
userInfoURL: settings.oidc.userInfoURL,
clientID: settings.oidc.clientID,
clientSecret: settings.oidc.clientSecret,
callbackURL: "/oidc/callback",
scope: "openid email profile"
}
return
}
this.oidc = { this.oidc = {
issuer: process.env.OIDC_ISSUER || '', issuer: process.env.OIDC_ISSUER || '',
authorizationURL: process.env.OIDC_AUTHORIZATION_URL || '', authorizationURL: process.env.OIDC_AUTHORIZATION_URL || '',
@ -34,17 +63,28 @@ class SSOSettings {
} }
toJSON() { toJSON() {
return { const tmp = {
id: this.id, id: this.id,
oidc: { ...this.oidc }, oidc: { ...this.oidc },
createNewUser: this.createNewUser, user: { ...this.user }
userPermissions: { ...this.userPermissions }
} }
return tmp
} }
update(payload) { update(payload) {
let hasUpdates = false const oldTmp = JSON.stringify(this.toJSON())
const newTmp = JSON.stringify(payload) // deep copy "for free"
const hasUpdates = difference(oldTmp, newTmp) !== ""; // Not very efficient, but ok for small objects
Logger.debug(`SSOSettings hasUpdates=${hasUpdates}`)
if (!hasUpdates) return hasUpdates
payload = JSON.parse(newTmp)
this.oidc = payload.oidc
this.user = payload.user
return hasUpdates
Logger.debug("SSOSettings.update", payload, this)
for (const key in payload) { for (const key in payload) {
Logger.debug(`key: ${key}: ${JSON.stringify(payload[key])}`)
if (isObject(payload[key])) { if (isObject(payload[key])) {
for (const setting in payload[key]) { for (const setting in payload[key]) {
if (!this[key] || this[key][setting] === payload[key][setting]) { if (!this[key] || this[key][setting] === payload[key][setting]) {
@ -58,12 +98,13 @@ class SSOSettings {
hasUpdates = true hasUpdates = true
} }
} }
Logger.debug("SSOSettings.update", hasUpdates, this)
return hasUpdates return hasUpdates
} }
getNewUserPermissions() { getNewUserPermissions() {
return { return {
...this.userPermissions ...this.user.permissions
} }
} }

View File

@ -14,6 +14,7 @@ class User {
this.lastSeen = null this.lastSeen = null
this.createdAt = null this.createdAt = null
this.audiobooks = null this.audiobooks = null
this.ssoId = null
this.settings = {} this.settings = {}
this.permissions = {} this.permissions = {}
@ -84,6 +85,7 @@ class User {
toJSON() { toJSON() {
return { return {
id: this.id, id: this.id,
ssoId: this.ssoId,
username: this.username, username: this.username,
pash: this.pash, pash: this.pash,
type: this.type, type: this.type,
@ -138,6 +140,7 @@ class User {
this.type = user.type this.type = user.type
this.stream = user.stream || null this.stream = user.stream || null
this.token = user.token this.token = user.token
this.ssoId = user.ssoId || ""
if (user.audiobooks) { if (user.audiobooks) {
this.audiobooks = {} this.audiobooks = {}
for (const key in user.audiobooks) { for (const key in user.audiobooks) {