Skip to content

Commit

Permalink
[#202] Implement roles in internal authorization and save it to the r…
Browse files Browse the repository at this point in the history
…epository
  • Loading branch information
palagdan committed Sep 5, 2024
1 parent eca01e5 commit c94b109
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 20 deletions.
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"react-promise-tracker": "^2.1.1",
"react-redux": "^7.2.4",
"react-router-dom": "^5.2.0",
"react-select": "^5.8.0",
"redux": "^4.1.0",
"redux-devtools-extension": "^2.13.9",
"redux-thunk": "^2.3.0",
Expand Down
61 changes: 61 additions & 0 deletions src/RoleSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React, { useState, useEffect } from "react";
import Select from "react-select";
import PropTypes from "prop-types";
import { ROLE } from "./constants/DefaultConstants.js";
import Row from "react-bootstrap/Row";
import { Col, FormGroup, FormLabel } from "react-bootstrap";

const roleOptions = Object.keys(ROLE).map((key) => ({
value: ROLE[key],
label: ROLE[key],
}));

const RoleSelector = ({ selected = [], handler, readOnly = true, label = "Roles" }) => {
const formatSelected = (selected) => {
return selected.map((value) => ({
value: value,
label: value,
}));
};

const [selectedRoles, setSelectedRoles] = useState(formatSelected(selected));

useEffect(() => {
setSelectedRoles(formatSelected(selected));
}, [selected]);

const handleChange = (selectedOptions) => {
setSelectedRoles(selectedOptions);
const selectedValues = selectedOptions.map((option) => option.value);
handler(selectedValues);
};

return (
<FormGroup as={Row}>
<Col as={FormLabel} lg={2} className="font-weight-bold text-lg-right align-self-center">
{label}
</Col>
<Col lg={10}>
<Select
value={selectedRoles}
onChange={handleChange}
isMulti
name="roles"
options={roleOptions}
isDisabled={readOnly}
className="basic-multi-select"
classNamePrefix="select"
/>
</Col>
</FormGroup>
);
};

RoleSelector.propTypes = {
selected: PropTypes.array,
handler: PropTypes.func.isRequired,
readOnly: PropTypes.bool,
label: PropTypes.string,
};

export default RoleSelector;
37 changes: 20 additions & 17 deletions src/components/user/User.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,16 @@ import withI18n from "../../i18n/withI18n";
import { injectIntl } from "react-intl";
import HorizontalInput from "../HorizontalInput";
import UserValidator from "../../validation/UserValidator";
import { ACTION_STATUS, ROLE } from "../../constants/DefaultConstants";
import { ACTION_STATUS, ROLE, ROLE_TYPE } from "../../constants/DefaultConstants";
import { getRole, processInstitutions } from "../../utils/Utils";
import * as Vocabulary from "../../constants/Vocabulary";
import { LoaderCard, LoaderSmall } from "../Loader";
import HelpIcon from "../HelpIcon";
import PropTypes from "prop-types";
import { FaRandom } from "react-icons/fa";
import { isUsingOidcAuth } from "../../utils/OidcUtils";
import { isAdmin } from "../../utils/SecurityUtils";
import { getRoles, isAdmin } from "../../utils/SecurityUtils";
import RoleSelector from "../../RoleSelector.jsx";

class User extends React.Component {
static propTypes = {
Expand All @@ -38,6 +39,7 @@ class User extends React.Component {
this.i18n = this.props.i18n;
this.formatMessage = this.props.formatMessage;
this.state = { savedWithEmail: false };
this._onRoleSelected = this._onRoleSelected.bind(this);
}

_onChange = (e) => {
Expand Down Expand Up @@ -160,7 +162,7 @@ class User extends React.Component {

_impersonateButton() {
const { user, currentUser, handlers, impersonation } = this.props;
if (!user.isNew && isAdmin(currentUser) && getRole(user) !== ROLE.ADMIN) {
if (!user.isNew && isAdmin(currentUser) && !isAdmin(user)) {
return (
<Button
style={{ margin: "0 0.3em 0 0" }}
Expand Down Expand Up @@ -201,6 +203,13 @@ class User extends React.Component {
}
}

_onRoleSelected(roles) {
const types = roles.map((role) => {
return ROLE_TYPE[role];
});
this.props.handlers.onChange({ types: types });
}

_onSaveAndSendEmail() {
this.props.handlers.onSave();
this.setState({ savedWithEmail: true });
Expand Down Expand Up @@ -302,20 +311,6 @@ class User extends React.Component {
</HorizontalInput>
</div>
)}
<div className="col-12 col-sm-6">
<HorizontalInput
type="select"
name="role"
label={`${this.i18n("user.role")}*`}
onChange={this._onAdminStatusChange}
disabled={!isAdmin(currentUser) || isUsingOidcAuth()}
value={user.types && getRole(user)}
labelWidth={3}
inputWidth={8}
>
{this._generateRolesOptions()}
</HorizontalInput>
</div>
</div>
{user.isNew && (
<div className="row">
Expand All @@ -332,6 +327,14 @@ class User extends React.Component {
</div>
</div>
)}
<div className="col-12 col-sm-8">
<RoleSelector
selected={getRoles(user)}
handler={this._onRoleSelected}
readOnly={(!isAdmin(currentUser) && currentUser.username !== user.username) || isUsingOidcAuth()}
label={this.i18n("user.roles")}
/>
</div>
<div className="buttons-line-height mt-3 text-center">
{this._impersonateButton()}
{isUsingOidcAuth() ? this._redirectToKeycloakButton() : this._passwordChangeButton()}
Expand Down
1 change: 1 addition & 0 deletions src/constants/DefaultConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const TYPE_ROLE = {
[Vocabulary.EDIT_USERS_TYPE]: ROLE.EDIT_USERS,
[Vocabulary.IMPORT_CODELISTS_TYPE]: ROLE.IMPORT_CODELISTS,
};
export const ROLE_TYPE = Object.fromEntries(Object.entries(TYPE_ROLE).map(([key, value]) => [value, key]));

// Default number of table elements per page.
export const DEFAULT_PAGE_SIZE = 10;
Expand Down
1 change: 1 addition & 0 deletions src/i18n/cs.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default {
"user.password-confirm": "Potvrzení hesla",
"user.passwords-not-matching-tooltip": "Hesla se neshodují",
"user.role": "Role",
"user.roles": "Roles",
"user.save-success": "Uživatel úspěšně uložen",
"user.save-success-with-email": "Uživatel úspěšně uložen a informován emailem.",
"user.save-error": "Uživatele se nepodařilo uložit. {error}",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default {
"user.password-confirm": "Confirm password",
"user.passwords-not-matching-tooltip": "Passwords don't match",
"user.role": "Role",
"user.roles": "Roles",
"user.save-success": "User saved successfully",
"user.save-success-with-email": "User saved successfully and informed by email.",
"user.save-error": "Unable to save user. {error}",
Expand Down
9 changes: 6 additions & 3 deletions src/utils/SecurityUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ export function clearToken() {
sessionStorage.removeItem(getOidcIdentityStorageKey());
}

export function isAdmin(currentUser) {
return currentUser.roles ? currentUser.roles.includes(ROLE.ADMIN) : false;
export function isAdmin(user) {
if (user.roles) {
return user.roles.includes(ROLE.ADMIN);
}
return user.types ? getRoles(user).includes(ROLE.ADMIN) : false;
}

export function hasRole(currentUser, role) {
Expand All @@ -30,7 +33,7 @@ export function isImpersonator(currentUser) {
}

export function getRoles(user) {
if (!user) {
if (!user || !user.types) {
return undefined;
}
let roles = [];
Expand Down

0 comments on commit c94b109

Please sign in to comment.