mirror of
https://github.com/plankanban/planka.git
synced 2025-07-18 20:59:44 +02:00
feat: Add ability to map OIDC attributes and ignore username
Closes #554
This commit is contained in:
parent
32ce07a843
commit
def2327165
13 changed files with 112 additions and 72 deletions
|
@ -24,6 +24,7 @@ const AccountPane = React.memo(
|
||||||
organization,
|
organization,
|
||||||
language,
|
language,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isUsernameLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
|
@ -104,7 +105,7 @@ const AccountPane = React.memo(
|
||||||
value={language || 'auto'}
|
value={language || 'auto'}
|
||||||
onChange={handleLanguageChange}
|
onChange={handleLanguageChange}
|
||||||
/>
|
/>
|
||||||
{!isLocked && (
|
{(!isLocked || !isUsernameLocked) && (
|
||||||
<>
|
<>
|
||||||
<Divider horizontal section>
|
<Divider horizontal section>
|
||||||
<Header as="h4">
|
<Header as="h4">
|
||||||
|
@ -113,56 +114,62 @@ const AccountPane = React.memo(
|
||||||
})}
|
})}
|
||||||
</Header>
|
</Header>
|
||||||
</Divider>
|
</Divider>
|
||||||
<div className={styles.action}>
|
{!isUsernameLocked && (
|
||||||
<UserUsernameEditPopup
|
<div className={styles.action}>
|
||||||
usePasswordConfirmation
|
<UserUsernameEditPopup
|
||||||
defaultData={usernameUpdateForm.data}
|
defaultData={usernameUpdateForm.data}
|
||||||
username={username}
|
username={username}
|
||||||
isSubmitting={usernameUpdateForm.isSubmitting}
|
isSubmitting={usernameUpdateForm.isSubmitting}
|
||||||
error={usernameUpdateForm.error}
|
error={usernameUpdateForm.error}
|
||||||
onUpdate={onUsernameUpdate}
|
usePasswordConfirmation={!isLocked} // FIXME: hack
|
||||||
onMessageDismiss={onUsernameUpdateMessageDismiss}
|
onUpdate={onUsernameUpdate}
|
||||||
>
|
onMessageDismiss={onUsernameUpdateMessageDismiss}
|
||||||
<Button className={styles.actionButton}>
|
>
|
||||||
{t('action.editUsername', {
|
<Button className={styles.actionButton}>
|
||||||
context: 'title',
|
{t('action.editUsername', {
|
||||||
})}
|
context: 'title',
|
||||||
</Button>
|
})}
|
||||||
</UserUsernameEditPopup>
|
</Button>
|
||||||
</div>
|
</UserUsernameEditPopup>
|
||||||
<div className={styles.action}>
|
</div>
|
||||||
<UserEmailEditPopup
|
)}
|
||||||
usePasswordConfirmation
|
{!isLocked && (
|
||||||
defaultData={emailUpdateForm.data}
|
<>
|
||||||
email={email}
|
<div className={styles.action}>
|
||||||
isSubmitting={emailUpdateForm.isSubmitting}
|
<UserEmailEditPopup
|
||||||
error={emailUpdateForm.error}
|
usePasswordConfirmation
|
||||||
onUpdate={onEmailUpdate}
|
defaultData={emailUpdateForm.data}
|
||||||
onMessageDismiss={onEmailUpdateMessageDismiss}
|
email={email}
|
||||||
>
|
isSubmitting={emailUpdateForm.isSubmitting}
|
||||||
<Button className={styles.actionButton}>
|
error={emailUpdateForm.error}
|
||||||
{t('action.editEmail', {
|
onUpdate={onEmailUpdate}
|
||||||
context: 'title',
|
onMessageDismiss={onEmailUpdateMessageDismiss}
|
||||||
})}
|
>
|
||||||
</Button>
|
<Button className={styles.actionButton}>
|
||||||
</UserEmailEditPopup>
|
{t('action.editEmail', {
|
||||||
</div>
|
context: 'title',
|
||||||
<div className={styles.action}>
|
})}
|
||||||
<UserPasswordEditPopup
|
</Button>
|
||||||
usePasswordConfirmation
|
</UserEmailEditPopup>
|
||||||
defaultData={passwordUpdateForm.data}
|
</div>
|
||||||
isSubmitting={passwordUpdateForm.isSubmitting}
|
<div className={styles.action}>
|
||||||
error={passwordUpdateForm.error}
|
<UserPasswordEditPopup
|
||||||
onUpdate={onPasswordUpdate}
|
usePasswordConfirmation
|
||||||
onMessageDismiss={onPasswordUpdateMessageDismiss}
|
defaultData={passwordUpdateForm.data}
|
||||||
>
|
isSubmitting={passwordUpdateForm.isSubmitting}
|
||||||
<Button className={styles.actionButton}>
|
error={passwordUpdateForm.error}
|
||||||
{t('action.editPassword', {
|
onUpdate={onPasswordUpdate}
|
||||||
context: 'title',
|
onMessageDismiss={onPasswordUpdateMessageDismiss}
|
||||||
})}
|
>
|
||||||
</Button>
|
<Button className={styles.actionButton}>
|
||||||
</UserPasswordEditPopup>
|
{t('action.editPassword', {
|
||||||
</div>
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Button>
|
||||||
|
</UserPasswordEditPopup>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
|
@ -179,6 +186,7 @@ AccountPane.propTypes = {
|
||||||
organization: PropTypes.string,
|
organization: PropTypes.string,
|
||||||
language: PropTypes.string,
|
language: PropTypes.string,
|
||||||
isLocked: PropTypes.bool.isRequired,
|
isLocked: PropTypes.bool.isRequired,
|
||||||
|
isUsernameLocked: PropTypes.bool.isRequired,
|
||||||
isAvatarUpdating: PropTypes.bool.isRequired,
|
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
usernameUpdateForm: PropTypes.object.isRequired,
|
usernameUpdateForm: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -18,6 +18,7 @@ const UserSettingsModal = React.memo(
|
||||||
language,
|
language,
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isUsernameLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
usernameUpdateForm,
|
usernameUpdateForm,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
|
@ -50,6 +51,7 @@ const UserSettingsModal = React.memo(
|
||||||
organization={organization}
|
organization={organization}
|
||||||
language={language}
|
language={language}
|
||||||
isLocked={isLocked}
|
isLocked={isLocked}
|
||||||
|
isUsernameLocked={isUsernameLocked}
|
||||||
isAvatarUpdating={isAvatarUpdating}
|
isAvatarUpdating={isAvatarUpdating}
|
||||||
usernameUpdateForm={usernameUpdateForm}
|
usernameUpdateForm={usernameUpdateForm}
|
||||||
emailUpdateForm={emailUpdateForm}
|
emailUpdateForm={emailUpdateForm}
|
||||||
|
@ -108,6 +110,7 @@ UserSettingsModal.propTypes = {
|
||||||
language: PropTypes.string,
|
language: PropTypes.string,
|
||||||
subscribeToOwnCards: PropTypes.bool.isRequired,
|
subscribeToOwnCards: PropTypes.bool.isRequired,
|
||||||
isLocked: PropTypes.bool.isRequired,
|
isLocked: PropTypes.bool.isRequired,
|
||||||
|
isUsernameLocked: PropTypes.bool.isRequired,
|
||||||
isAvatarUpdating: PropTypes.bool.isRequired,
|
isAvatarUpdating: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
usernameUpdateForm: PropTypes.object.isRequired,
|
usernameUpdateForm: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -136,13 +136,15 @@ const ActionsStep = React.memo(
|
||||||
context: 'title',
|
context: 'title',
|
||||||
})}
|
})}
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
{!user.isUsernameLocked && (
|
||||||
|
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
|
||||||
|
{t('action.editUsername', {
|
||||||
|
context: 'title',
|
||||||
|
})}
|
||||||
|
</Menu.Item>
|
||||||
|
)}
|
||||||
{!user.isLocked && (
|
{!user.isLocked && (
|
||||||
<>
|
<>
|
||||||
<Menu.Item className={styles.menuItem} onClick={handleEditUsernameClick}>
|
|
||||||
{t('action.editUsername', {
|
|
||||||
context: 'title',
|
|
||||||
})}
|
|
||||||
</Menu.Item>
|
|
||||||
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
|
<Menu.Item className={styles.menuItem} onClick={handleEditEmailClick}>
|
||||||
{t('action.editEmail', {
|
{t('action.editEmail', {
|
||||||
context: 'title',
|
context: 'title',
|
||||||
|
|
|
@ -19,6 +19,7 @@ const Item = React.memo(
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isLocked,
|
isLocked,
|
||||||
isRoleLocked,
|
isRoleLocked,
|
||||||
|
isUsernameLocked,
|
||||||
isDeletionLocked,
|
isDeletionLocked,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
|
@ -61,6 +62,7 @@ const Item = React.memo(
|
||||||
phone,
|
phone,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isUsernameLocked,
|
||||||
isDeletionLocked,
|
isDeletionLocked,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
|
@ -95,6 +97,7 @@ Item.propTypes = {
|
||||||
isAdmin: PropTypes.bool.isRequired,
|
isAdmin: PropTypes.bool.isRequired,
|
||||||
isLocked: PropTypes.bool.isRequired,
|
isLocked: PropTypes.bool.isRequired,
|
||||||
isRoleLocked: PropTypes.bool.isRequired,
|
isRoleLocked: PropTypes.bool.isRequired,
|
||||||
|
isUsernameLocked: PropTypes.bool.isRequired,
|
||||||
isDeletionLocked: PropTypes.bool.isRequired,
|
isDeletionLocked: PropTypes.bool.isRequired,
|
||||||
/* eslint-disable react/forbid-prop-types */
|
/* eslint-disable react/forbid-prop-types */
|
||||||
emailUpdateForm: PropTypes.object.isRequired,
|
emailUpdateForm: PropTypes.object.isRequired,
|
||||||
|
|
|
@ -112,6 +112,7 @@ const UsersModal = React.memo(
|
||||||
isAdmin={item.isAdmin}
|
isAdmin={item.isAdmin}
|
||||||
isLocked={item.isLocked}
|
isLocked={item.isLocked}
|
||||||
isRoleLocked={item.isRoleLocked}
|
isRoleLocked={item.isRoleLocked}
|
||||||
|
isUsernameLocked={item.isUsernameLocked}
|
||||||
isDeletionLocked={item.isDeletionLocked}
|
isDeletionLocked={item.isDeletionLocked}
|
||||||
emailUpdateForm={item.emailUpdateForm}
|
emailUpdateForm={item.emailUpdateForm}
|
||||||
passwordUpdateForm={item.passwordUpdateForm}
|
passwordUpdateForm={item.passwordUpdateForm}
|
||||||
|
|
|
@ -16,6 +16,7 @@ const mapStateToProps = (state) => {
|
||||||
language,
|
language,
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isUsernameLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
|
@ -32,6 +33,7 @@ const mapStateToProps = (state) => {
|
||||||
language,
|
language,
|
||||||
subscribeToOwnCards,
|
subscribeToOwnCards,
|
||||||
isLocked,
|
isLocked,
|
||||||
|
isUsernameLocked,
|
||||||
isAvatarUpdating,
|
isAvatarUpdating,
|
||||||
emailUpdateForm,
|
emailUpdateForm,
|
||||||
passwordUpdateForm,
|
passwordUpdateForm,
|
||||||
|
|
|
@ -46,6 +46,7 @@ export default class extends BaseModel {
|
||||||
isAdmin: attr(),
|
isAdmin: attr(),
|
||||||
isLocked: attr(),
|
isLocked: attr(),
|
||||||
isRoleLocked: attr(),
|
isRoleLocked: attr(),
|
||||||
|
isUsernameLocked: attr(),
|
||||||
isDeletionLocked: attr(),
|
isDeletionLocked: attr(),
|
||||||
deletedAt: attr(),
|
deletedAt: attr(),
|
||||||
createdAt: attr({
|
createdAt: attr({
|
||||||
|
|
|
@ -44,7 +44,11 @@ services:
|
||||||
# - OIDC_CLIENT_SECRET=
|
# - OIDC_CLIENT_SECRET=
|
||||||
# - OIDC_SCOPES=openid email profile
|
# - OIDC_SCOPES=openid email profile
|
||||||
# - OIDC_ADMIN_ROLES=admin
|
# - OIDC_ADMIN_ROLES=admin
|
||||||
|
# - OIDC_EMAIL_ATTRIBUTE=email
|
||||||
|
# - OIDC_NAME_ATTRIBUTE=name
|
||||||
|
# - OIDC_USERNAME_ATTRIBUTE=preferred_username
|
||||||
# - OIDC_ROLES_ATTRIBUTE=groups
|
# - OIDC_ROLES_ATTRIBUTE=groups
|
||||||
|
# - OIDC_IGNORE_USERNAME=true
|
||||||
# - OIDC_IGNORE_ROLES=true
|
# - OIDC_IGNORE_ROLES=true
|
||||||
|
|
||||||
# - SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx
|
# - SLACK_BOT_TOKEN=xoxb-xxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxx
|
||||||
|
|
|
@ -27,7 +27,11 @@ SECRET_KEY=notsecretkey
|
||||||
# OIDC_CLIENT_SECRET=
|
# OIDC_CLIENT_SECRET=
|
||||||
# OIDC_SCOPES=openid email profile
|
# OIDC_SCOPES=openid email profile
|
||||||
# OIDC_ADMIN_ROLES=admin
|
# OIDC_ADMIN_ROLES=admin
|
||||||
|
# OIDC_EMAIL_ATTRIBUTE=email
|
||||||
|
# OIDC_NAME_ATTRIBUTE=name
|
||||||
|
# OIDC_USERNAME_ATTRIBUTE=preferred_username
|
||||||
# OIDC_ROLES_ATTRIBUTE=groups
|
# OIDC_ROLES_ATTRIBUTE=groups
|
||||||
|
# OIDC_IGNORE_USERNAME=true
|
||||||
# OIDC_IGNORE_ROLES=true
|
# OIDC_IGNORE_ROLES=true
|
||||||
|
|
||||||
## Do not edit this
|
## Do not edit this
|
||||||
|
|
|
@ -53,11 +53,7 @@ module.exports = {
|
||||||
async fn(inputs) {
|
async fn(inputs) {
|
||||||
const { currentUser } = this.req;
|
const { currentUser } = this.req;
|
||||||
|
|
||||||
if (inputs.id === currentUser.id) {
|
if (inputs.id !== currentUser.id && !currentUser.isAdmin) {
|
||||||
if (!inputs.currentPassword) {
|
|
||||||
throw Errors.INVALID_CURRENT_PASSWORD;
|
|
||||||
}
|
|
||||||
} else if (!currentUser.isAdmin) {
|
|
||||||
throw Errors.USER_NOT_FOUND; // Forbidden
|
throw Errors.USER_NOT_FOUND; // Forbidden
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,15 +63,18 @@ module.exports = {
|
||||||
throw Errors.USER_NOT_FOUND;
|
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;
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (user.isSso) {
|
||||||
inputs.id === currentUser.id &&
|
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||||
!bcrypt.compareSync(inputs.currentPassword, user.password)
|
throw Errors.NOT_ENOUGH_RIGHTS;
|
||||||
) {
|
}
|
||||||
throw Errors.INVALID_CURRENT_PASSWORD;
|
} 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']);
|
const values = _.pick(inputs, ['username']);
|
||||||
|
|
|
@ -38,7 +38,10 @@ module.exports = {
|
||||||
throw 'invalidCodeOrNonce';
|
throw 'invalidCodeOrNonce';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userInfo.email || !userInfo.name) {
|
if (
|
||||||
|
!userInfo[sails.config.custom.oidcEmailAttribute] ||
|
||||||
|
!userInfo[sails.config.custom.oidcNameAttribute]
|
||||||
|
) {
|
||||||
throw 'missingValues';
|
throw 'missingValues';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,12 +59,14 @@ module.exports = {
|
||||||
|
|
||||||
const values = {
|
const values = {
|
||||||
isAdmin,
|
isAdmin,
|
||||||
email: userInfo.email,
|
email: userInfo[sails.config.custom.oidcEmailAttribute],
|
||||||
isSso: true,
|
isSso: true,
|
||||||
name: userInfo.name,
|
name: userInfo[sails.config.custom.oidcNameAttribute],
|
||||||
username: userInfo.preferred_username,
|
|
||||||
subscribeToOwnCards: false,
|
subscribeToOwnCards: false,
|
||||||
};
|
};
|
||||||
|
if (!sails.config.custom.oidcIgnoreUsername) {
|
||||||
|
values.username = userInfo[sails.config.custom.oidcUsernameAttribute];
|
||||||
|
}
|
||||||
|
|
||||||
let user;
|
let user;
|
||||||
// This whole block technically needs to be executed in a transaction
|
// 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) {
|
if (!sails.config.custom.oidcIgnoreRoles) {
|
||||||
updateFieldKeys.push('isAdmin');
|
updateFieldKeys.push('isAdmin');
|
||||||
}
|
}
|
||||||
|
|
|
@ -116,6 +116,7 @@ module.exports = {
|
||||||
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
|
..._.omit(this, ['password', 'isSso', 'avatar', 'passwordChangedAt']),
|
||||||
isLocked: this.isSso || isDefaultAdmin,
|
isLocked: this.isSso || isDefaultAdmin,
|
||||||
isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin,
|
isRoleLocked: (this.isSso && !sails.config.custom.oidcIgnoreRoles) || isDefaultAdmin,
|
||||||
|
isUsernameLocked: (this.isSso && !sails.config.custom.oidcIgnoreUsername) || isDefaultAdmin,
|
||||||
isDeletionLocked: isDefaultAdmin,
|
isDeletionLocked: isDefaultAdmin,
|
||||||
avatarUrl:
|
avatarUrl:
|
||||||
this.avatar &&
|
this.avatar &&
|
||||||
|
|
|
@ -38,7 +38,11 @@ module.exports.custom = {
|
||||||
oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
|
oidcClientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||||
oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
|
oidcScopes: process.env.OIDC_SCOPES || 'openid email profile',
|
||||||
oidcAdminRoles: process.env.OIDC_ADMIN_ROLES ? process.env.OIDC_ADMIN_ROLES.split(',') : [],
|
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',
|
oidcRolesAttribute: process.env.OIDC_ROLES_ATTRIBUTE || 'groups',
|
||||||
|
oidcIgnoreUsername: process.env.OIDC_IGNORE_USERNAME === 'true',
|
||||||
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
|
oidcIgnoreRoles: process.env.OIDC_IGNORE_ROLES === 'true',
|
||||||
|
|
||||||
// TODO: move client base url to environment variable?
|
// TODO: move client base url to environment variable?
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue