diff --git a/client/src/components/UserSettingsModal/AccountPane/AccountPane.jsx b/client/src/components/UserSettingsModal/AccountPane/AccountPane.jsx index 7333cf5d..eed13261 100644 --- a/client/src/components/UserSettingsModal/AccountPane/AccountPane.jsx +++ b/client/src/components/UserSettingsModal/AccountPane/AccountPane.jsx @@ -24,6 +24,7 @@ const AccountPane = React.memo( organization, language, isLocked, + isUsernameLocked, isAvatarUpdating, usernameUpdateForm, emailUpdateForm, @@ -104,7 +105,7 @@ const AccountPane = React.memo( value={language || 'auto'} onChange={handleLanguageChange} /> - {!isLocked && ( + {(!isLocked || !isUsernameLocked) && ( <>
@@ -113,56 +114,62 @@ const AccountPane = React.memo( })}
-
- - - -
-
- - - -
-
- - - -
+ {!isUsernameLocked && ( +
+ + + +
+ )} + {!isLocked && ( + <> +
+ + + +
+
+ + + +
+ + )} )} @@ -179,6 +186,7 @@ AccountPane.propTypes = { organization: PropTypes.string, language: PropTypes.string, isLocked: PropTypes.bool.isRequired, + isUsernameLocked: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired, /* eslint-disable react/forbid-prop-types */ usernameUpdateForm: PropTypes.object.isRequired, diff --git a/client/src/components/UserSettingsModal/UserSettingsModal.jsx b/client/src/components/UserSettingsModal/UserSettingsModal.jsx index fc8ac9c7..8767ac4e 100644 --- a/client/src/components/UserSettingsModal/UserSettingsModal.jsx +++ b/client/src/components/UserSettingsModal/UserSettingsModal.jsx @@ -18,6 +18,7 @@ const UserSettingsModal = React.memo( language, subscribeToOwnCards, isLocked, + isUsernameLocked, isAvatarUpdating, usernameUpdateForm, emailUpdateForm, @@ -50,6 +51,7 @@ const UserSettingsModal = React.memo( organization={organization} language={language} isLocked={isLocked} + isUsernameLocked={isUsernameLocked} isAvatarUpdating={isAvatarUpdating} usernameUpdateForm={usernameUpdateForm} emailUpdateForm={emailUpdateForm} @@ -108,6 +110,7 @@ UserSettingsModal.propTypes = { language: PropTypes.string, subscribeToOwnCards: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired, + isUsernameLocked: PropTypes.bool.isRequired, isAvatarUpdating: PropTypes.bool.isRequired, /* eslint-disable react/forbid-prop-types */ usernameUpdateForm: PropTypes.object.isRequired, diff --git a/client/src/components/UsersModal/Item/ActionsStep.jsx b/client/src/components/UsersModal/Item/ActionsStep.jsx index f8a2959b..7b0eaed6 100644 --- a/client/src/components/UsersModal/Item/ActionsStep.jsx +++ b/client/src/components/UsersModal/Item/ActionsStep.jsx @@ -136,13 +136,15 @@ const ActionsStep = React.memo( context: 'title', })} + {!user.isUsernameLocked && ( + + {t('action.editUsername', { + context: 'title', + })} + + )} {!user.isLocked && ( <> - - {t('action.editUsername', { - context: 'title', - })} - {t('action.editEmail', { context: 'title', diff --git a/client/src/components/UsersModal/Item/Item.jsx b/client/src/components/UsersModal/Item/Item.jsx index 80db3568..ed68da3a 100755 --- a/client/src/components/UsersModal/Item/Item.jsx +++ b/client/src/components/UsersModal/Item/Item.jsx @@ -19,6 +19,7 @@ const Item = React.memo( isAdmin, isLocked, isRoleLocked, + isUsernameLocked, isDeletionLocked, emailUpdateForm, passwordUpdateForm, @@ -61,6 +62,7 @@ const Item = React.memo( phone, isAdmin, isLocked, + isUsernameLocked, isDeletionLocked, emailUpdateForm, passwordUpdateForm, @@ -95,6 +97,7 @@ Item.propTypes = { isAdmin: PropTypes.bool.isRequired, isLocked: PropTypes.bool.isRequired, isRoleLocked: PropTypes.bool.isRequired, + isUsernameLocked: PropTypes.bool.isRequired, isDeletionLocked: PropTypes.bool.isRequired, /* eslint-disable react/forbid-prop-types */ emailUpdateForm: PropTypes.object.isRequired, diff --git a/client/src/components/UsersModal/UsersModal.jsx b/client/src/components/UsersModal/UsersModal.jsx index 0b55778e..280cd08a 100755 --- a/client/src/components/UsersModal/UsersModal.jsx +++ b/client/src/components/UsersModal/UsersModal.jsx @@ -112,6 +112,7 @@ const UsersModal = React.memo( isAdmin={item.isAdmin} isLocked={item.isLocked} isRoleLocked={item.isRoleLocked} + isUsernameLocked={item.isUsernameLocked} isDeletionLocked={item.isDeletionLocked} emailUpdateForm={item.emailUpdateForm} passwordUpdateForm={item.passwordUpdateForm} diff --git a/client/src/containers/UserSettingsModalContainer.js b/client/src/containers/UserSettingsModalContainer.js index 620632e0..8c3b1c36 100644 --- a/client/src/containers/UserSettingsModalContainer.js +++ b/client/src/containers/UserSettingsModalContainer.js @@ -16,6 +16,7 @@ const mapStateToProps = (state) => { language, subscribeToOwnCards, isLocked, + isUsernameLocked, isAvatarUpdating, emailUpdateForm, passwordUpdateForm, @@ -32,6 +33,7 @@ const mapStateToProps = (state) => { language, subscribeToOwnCards, isLocked, + isUsernameLocked, isAvatarUpdating, emailUpdateForm, passwordUpdateForm, diff --git a/client/src/models/User.js b/client/src/models/User.js index 84555308..9bf8208f 100755 --- a/client/src/models/User.js +++ b/client/src/models/User.js @@ -46,6 +46,7 @@ export default class extends BaseModel { isAdmin: attr(), isLocked: attr(), isRoleLocked: attr(), + isUsernameLocked: attr(), isDeletionLocked: attr(), deletedAt: attr(), createdAt: attr({ diff --git a/docker-compose.yml b/docker-compose.yml index 9d8fef1e..01016dc9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,11 @@ services: # - OIDC_CLIENT_SECRET= # - OIDC_SCOPES=openid email profile # - OIDC_ADMIN_ROLES=admin + # - OIDC_EMAIL_ATTRIBUTE=email + # - OIDC_NAME_ATTRIBUTE=name + # - OIDC_USERNAME_ATTRIBUTE=preferred_username # - OIDC_ROLES_ATTRIBUTE=groups + # - OIDC_IGNORE_USERNAME=true # - OIDC_IGNORE_ROLES=true depends_on: - postgres diff --git a/server/.env.sample b/server/.env.sample index f06b6470..31f2342b 100644 --- a/server/.env.sample +++ b/server/.env.sample @@ -27,7 +27,11 @@ SECRET_KEY=notsecretkey # OIDC_CLIENT_SECRET= # OIDC_SCOPES=openid email profile # OIDC_ADMIN_ROLES=admin +# OIDC_EMAIL_ATTRIBUTE=email +# OIDC_NAME_ATTRIBUTE=name +# OIDC_USERNAME_ATTRIBUTE=preferred_username # OIDC_ROLES_ATTRIBUTE=groups +# OIDC_IGNORE_USERNAME=true # OIDC_IGNORE_ROLES=true ## Do not edit this diff --git a/server/api/controllers/users/update-username.js b/server/api/controllers/users/update-username.js index 58059460..b55529b4 100644 --- a/server/api/controllers/users/update-username.js +++ b/server/api/controllers/users/update-username.js @@ -53,11 +53,7 @@ module.exports = { async fn(inputs) { const { currentUser } = this.req; - if (inputs.id === currentUser.id) { - if (!inputs.currentPassword) { - throw Errors.INVALID_CURRENT_PASSWORD; - } - } else if (!currentUser.isAdmin) { + if (inputs.id !== currentUser.id && !currentUser.isAdmin) { throw Errors.USER_NOT_FOUND; // Forbidden } @@ -67,15 +63,18 @@ module.exports = { throw Errors.USER_NOT_FOUND; } - if (user.email === sails.config.custom.defaultAdminEmail || user.isSso) { + if (user.email === sails.config.custom.defaultAdminEmail) { throw Errors.NOT_ENOUGH_RIGHTS; } - if ( - inputs.id === currentUser.id && - !bcrypt.compareSync(inputs.currentPassword, user.password) - ) { - throw Errors.INVALID_CURRENT_PASSWORD; + if (user.isSso) { + if (!sails.config.custom.oidcIgnoreUsername) { + throw Errors.NOT_ENOUGH_RIGHTS; + } + } else if (inputs.id === currentUser.id) { + if (!inputs.currentPassword || !bcrypt.compareSync(inputs.currentPassword, user.password)) { + throw Errors.INVALID_CURRENT_PASSWORD; + } } const values = _.pick(inputs, ['username']); diff --git a/server/api/helpers/users/get-or-create-one-using-oidc.js b/server/api/helpers/users/get-or-create-one-using-oidc.js index 6d1c49f0..2186c099 100644 --- a/server/api/helpers/users/get-or-create-one-using-oidc.js +++ b/server/api/helpers/users/get-or-create-one-using-oidc.js @@ -38,7 +38,10 @@ module.exports = { throw 'invalidCodeOrNonce'; } - if (!userInfo.email || !userInfo.name) { + if ( + !userInfo[sails.config.custom.oidcEmailAttribute] || + !userInfo[sails.config.custom.oidcNameAttribute] + ) { throw 'missingValues'; } @@ -56,12 +59,14 @@ module.exports = { const values = { isAdmin, - email: userInfo.email, + email: userInfo[sails.config.custom.oidcEmailAttribute], isSso: true, - name: userInfo.name, - username: userInfo.preferred_username, + name: userInfo[sails.config.custom.oidcNameAttribute], subscribeToOwnCards: false, }; + if (!sails.config.custom.oidcIgnoreUsername) { + values.username = userInfo[sails.config.custom.oidcUsernameAttribute]; + } let user; // This whole block technically needs to be executed in a transaction @@ -95,7 +100,10 @@ module.exports = { }); } - const updateFieldKeys = ['email', 'isSso', 'name', 'username']; + const updateFieldKeys = ['email', 'isSso', 'name']; + if (!sails.config.custom.oidcIgnoreUsername) { + updateFieldKeys.push('username'); + } if (!sails.config.custom.oidcIgnoreRoles) { updateFieldKeys.push('isAdmin'); } diff --git a/server/api/models/User.js b/server/api/models/User.js index 9bf8a298..1d875318 100755 --- a/server/api/models/User.js +++ b/server/api/models/User.js @@ -116,6 +116,7 @@ module.exports = { ..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']), isLocked: this.isSso || isDefaultAdmin, isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin, + isUsernameLocked: (this.isSso && !sails.config.custom.oidcIgnoreUsername) || isDefaultAdmin, isDeletionLocked: isDefaultAdmin, avatarUrl: this.avatar && diff --git a/server/config/custom.js b/server/config/custom.js index cbbc89ba..afd60ec4 100644 --- a/server/config/custom.js +++ b/server/config/custom.js @@ -38,7 +38,11 @@ module.exports.custom = { oidcClientSecret: process.env.OIDC_CLIENT_SECRET, oidcScopes: process.env.OIDC_SCOPES || 'openid email profile', oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [], + oidcEmailAttribute: process.env.OIDC_EMAIL_ATTRIBUTE || 'email', + oidcNameAttribute: process.env.OIDC_NAME_ATTRIBUTE || 'name', + oidcUsernameAttribute: process.env.OIDC_USERNAME_ATTRIBUTE || 'preferred_username', oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups', + oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true', oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true', // TODO: move client base url to environment variable?