Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fine grained access #208

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions deploy/shared/db-server/import/record-manager-app/role-groups.trig
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
@prefix : <http://onto.fel.cvut.cz/ontologies/record-manager/> .
@prefix rm: <http://onto.fel.cvut.cz/ontologies/record-manager/> .
@prefix doc: <http://onto.fel.cvut.cz/ontologies/documentation/> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix xml: <http://www.w3.org/XML/1998/namespace> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
@prefix form: <http://onto.fel.cvut.cz/ontologies/form/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix ufo: <http://onto.fel.cvut.cz/ontologies/ufo/> .

<http://onto.fel.cvut.cz/ontologies/record-manager/role-groups> {
rm:admin-role-group rdf:type owl:NamedIndividual, rm:role-group;
rm:has-role rm:RM_ADMIN,
rm:RM_USER,
rm:complete-records-role,
rm:delete-organization-records-role,
rm:edit-organization-records-role,
rm:view-organization-records-role,
rm:edit-users-role,
rm:import-codelists-role,
rm:reject-records-role,
rm:delete-all-records-role,
rm:edit-all-records-role,
rm:publish-records-role,
rm:view-all-records-role;
rdfs:label "admin-role-group"@en .

rm:user-role-group rdf:type owl:NamedIndividual, rm:role-group;
rm:has-role rm:RM_USER;
rdfs:label "user-role-group"@en .
}
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
60 changes: 60 additions & 0 deletions src/RoleSelector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
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={formatSelected(selected)}
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;
5 changes: 3 additions & 2 deletions src/actions/AuthActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import * as ActionConstants from "../constants/ActionConstants";
import { API_URL } from "../../config";
import { IMPERSONATOR_TYPE } from "../constants/Vocabulary";
import { IMPERSONATE_LOGOUT_SUCCESS, IMPERSONATE_PENDING } from "../constants/ActionConstants";
import { MediaType } from "../constants/DefaultConstants";
import { MediaType, ROLE } from "../constants/DefaultConstants";
import { hasRole } from "../utils/SecurityUtils.js";

export function login(username, password) {
return function (dispatch) {
Expand Down Expand Up @@ -56,7 +57,7 @@ export function userAuthError(error) {

export function logout() {
return function (dispatch, getState) {
if (getState().auth.user.types.indexOf(IMPERSONATOR_TYPE) !== -1) {
if (hasRole(getState().auth.user, ROLE.IMPERSONATE)) {
return logoutImpersonator(dispatch);
}
return axiosBackend
Expand Down
34 changes: 34 additions & 0 deletions src/actions/RoleGroupActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { asyncError, asyncRequest, showServerResponseErrorMessage } from "./AsyncActionUtils.js";
import * as ActionConstants from "../constants/ActionConstants.js";
import { axiosBackend } from "./index.js";
import { API_URL } from "../../config/index.js";

export function loadRoleGroups() {
return function (dispatch, getState) {
dispatch(loadRoleGroupsPending());
return axiosBackend
.get(`${API_URL}/rest/roleGroups`, {})
.then((response) => {
dispatch(loadRoleGroupsSuccess(response.data));
})
.catch((error) => {
dispatch(loadRoleGroupsError(Error(error.response.data)));
dispatch(showServerResponseErrorMessage(error, "roleGroup.loading-error"));
});
};
}

export function loadRoleGroupsPending() {
return asyncRequest(ActionConstants.LOAD_ROLE_GROUPS_PENDING);
}

export function loadRoleGroupsSuccess(roleGroups) {
return {
type: ActionConstants.LOAD_ROLE_GROUPS_SUCCESS,
roleGroups,
};
}

export function loadRoleGroupsError(error) {
return asyncError(ActionConstants.LOAD_ROLE_GROUPS_ERROR, error);
}
9 changes: 4 additions & 5 deletions src/components/MainView.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class MainView extends React.Component {
const path = this.props.location.pathname;

return (
<IfGranted expected={ROLE.ADMIN} actual={this.props.user.role}>
<IfGranted expected={ROLE.ADMIN} actual={this.props.user.roles}>
<NavItem>
<NavLink to={Routes.users.path} isActive={() => path.startsWith(Routes.users.path)} className="nav-link">
{this.i18n("main.users-nav")}
Expand Down Expand Up @@ -95,7 +95,6 @@ class MainView extends React.Component {
const user = this.props.user;
const name = user.firstName.substring(0, 1) + ". " + user.lastName;
const path = this.props.location.pathname;

return (
<div className="main-view-wrapper">
<header>
Expand Down Expand Up @@ -129,7 +128,7 @@ class MainView extends React.Component {
</NavLink>
</NavItem>
) : null}
<IfGranted expected={ROLE.ADMIN} actual={user.role}>
<IfGranted expected={ROLE.ADMIN} actual={user.roles}>
<NavItem>
<NavLink
className="nav-link"
Expand All @@ -140,10 +139,10 @@ class MainView extends React.Component {
</NavLink>
</NavItem>
</IfGranted>
<IfGranted expected={ROLE.ADMIN} actual={user.role}>
<IfGranted expected={ROLE.ADMIN} actual={user.roles}>
<NavItem>{this._renderStatisticsNavLink(path)}</NavItem>
</IfGranted>
<IfGranted expected={ROLE.ADMIN} actual={user.role}>
<IfGranted expected={ROLE.ADMIN} actual={user.roles}>
<NavItem>
<NavLink
className="nav-link"
Expand Down
2 changes: 1 addition & 1 deletion src/components/history/HistoryDetail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const HistoryDetail = () => {
}, [dispatch, key]);

return (
<IfGranted expected={ROLE.ADMIN} actual={currentUser.role}>
<IfGranted expected={ROLE.ADMIN} actual={currentUser.roles}>
<Card variant="primary">
<Card.Header className="text-light bg-primary" as="h6">
{i18n("history.panel-title")}
Expand Down
7 changes: 4 additions & 3 deletions src/components/institution/Institution.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { LoaderSmall } from "../Loader";
import InstitutionValidator from "../../validation/InstitutionValidator";
import HelpIcon from "../HelpIcon";
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import { isAdmin } from "../../utils/SecurityUtils.js";

/**
* Institution detail. Editable only for admins.
Expand Down Expand Up @@ -63,7 +64,7 @@ class Institution extends React.Component {
name="name"
label={`${this.i18n("institution.name")}*`}
value={institution.name}
readOnly={currentUser.role !== ROLE.ADMIN}
readOnly={!isAdmin(currentUser)}
onChange={this._onChange}
labelWidth={3}
inputWidth={8}
Expand All @@ -75,7 +76,7 @@ class Institution extends React.Component {
name="emailAddress"
label={this.i18n("institution.email")}
value={institution.emailAddress || ""}
readOnly={currentUser.role !== ROLE.ADMIN}
readOnly={!isAdmin(currentUser)}
onChange={this._onChange}
labelWidth={3}
inputWidth={8}
Expand Down Expand Up @@ -122,7 +123,7 @@ class Institution extends React.Component {

_renderButtons() {
const { currentUser, handlers, institutionSaved } = this.props;
if (currentUser.role !== ROLE.ADMIN) {
if (!isAdmin(currentUser)) {
return (
<div className="row justify-content-center">
<Button variant="primary" size="sm" className="action-button" onClick={handlers.onCancel}>
Expand Down
5 changes: 3 additions & 2 deletions src/components/institution/InstitutionsController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { bindActionCreators } from "redux";
import { deleteInstitution } from "../../actions/InstitutionActions";
import { trackPromise } from "react-promise-tracker";
import PropTypes from "prop-types";
import { isAdmin } from "../../utils/SecurityUtils.js";

class InstitutionsController extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -47,7 +48,7 @@ class InstitutionsController extends React.Component {

render() {
const { currentUser, institutionsLoaded, institutionDeleted } = this.props;
if (!currentUser || currentUser.role !== ROLE.ADMIN) {
if (!currentUser || !isAdmin(currentUser)) {
return null;
}
const handlers = {
Expand All @@ -70,7 +71,7 @@ InstitutionsController.propTypes = {
transitionToWithOpts: PropTypes.func.isRequired,
deleteInstitution: PropTypes.func.isRequired,
currentUser: PropTypes.shape({
role: PropTypes.string.isRequired,
roles: PropTypes.array.isRequired,
}).isRequired,
institutionsLoaded: PropTypes.object,
institutionDeleted: PropTypes.object,
Expand Down
9 changes: 4 additions & 5 deletions src/components/record/Record.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@ import HorizontalInput from "../HorizontalInput";
import RecordForm from "./RecordForm";
import RecordProvenance from "./RecordProvenance";
import RequiredAttributes from "./RequiredAttributes";
import { ACTION_STATUS, EXTENSION_CONSTANTS, RECORD_PHASE } from "../../constants/DefaultConstants";
import { ACTION_STATUS, RECORD_PHASE, ROLE } from "../../constants/DefaultConstants";
import { LoaderCard, LoaderSmall } from "../Loader";
import { processTypeaheadOptions } from "./TypeaheadAnswer";
import { EXTENSIONS } from "../../../config";
import { isAdmin } from "../../utils/SecurityUtils";
import { hasRole, isAdmin } from "../../utils/SecurityUtils";
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import { Constants as SConstants, FormUtils } from "@kbss-cvut/s-forms";
import FormValidationDialog from "../FormValidationDialog.jsx";
Expand Down Expand Up @@ -186,7 +185,7 @@ class Record extends React.Component {

return (
<div className="mt-3 text-center">
{EXTENSIONS === EXTENSION_CONSTANTS.SUPPLIER &&
{hasRole(this.props.currentUser, ROLE.REJECT_RECORDS) &&
!record.isNew &&
(record.phase === RECORD_PHASE.OPEN || this._isAdmin()) && (
<RejectButton
Expand All @@ -207,7 +206,7 @@ class Record extends React.Component {
</RejectButton>
)}

{(EXTENSIONS === EXTENSION_CONSTANTS.SUPPLIER || EXTENSIONS === EXTENSION_CONSTANTS.OPERATOR) &&
{hasRole(this.props.currentUser, ROLE.COMPLETE_RECORDS) &&
!record.isNew &&
(record.phase === RECORD_PHASE.OPEN || this._isAdmin()) && (
<Button
Expand Down
4 changes: 2 additions & 2 deletions src/components/record/RecordRow.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ const RecordRow = (props) => {

return (
<tr className="position-relative">
<IfGranted expected={ROLE.ADMIN} actual={props.currentUser.role}>
<IfGranted expected={ROLE.ADMIN} actual={props.currentUser.roles}>
<td className="report-row">
<Button variant="link" size="sm" onClick={() => props.onEdit(record)}>
{record.key}
Expand All @@ -59,7 +59,7 @@ const RecordRow = (props) => {
{record.localName}
</Button>
</td>
<IfGranted expected={ROLE.ADMIN} actual={props.currentUser.role}>
<IfGranted expected={ROLE.ADMIN} actual={props.currentUser.roles}>
<td className="report-row content-center">{record.institution.name}</td>
<td className="report-row content-center">
{getFormTemplateOptionName(record.formTemplate, formTemplateOptions)}
Expand Down
4 changes: 2 additions & 2 deletions src/components/record/RecordTable.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,11 @@ class RecordTable extends React.Component {
return (
<thead>
<tr>
<IfGranted expected={ROLE.ADMIN} actual={this.props.currentUser.role}>
<IfGranted expected={ROLE.ADMIN} actual={this.props.currentUser.roles}>
<th className="col-1 content-center">{this.i18n("records.id")}</th>
</IfGranted>
<th className="col-2 content-center">{this.i18n("records.local-name")}</th>
<IfGranted expected={ROLE.ADMIN} actual={this.props.currentUser.role}>
<IfGranted expected={ROLE.ADMIN} actual={this.props.currentUser.roles}>
<FilterableInstitutionHeader filters={filters} onFilterChange={onChange} />
<FilterableTemplateHeader filters={filters} onFilterChange={onChange} />
</IfGranted>
Expand Down
6 changes: 3 additions & 3 deletions src/components/record/Records.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import React from "react";
import { Alert, Button, Card } from "react-bootstrap";
import { injectIntl } from "react-intl";
import withI18n from "../../i18n/withI18n";
import { EXTENSION_CONSTANTS } from "../../constants/DefaultConstants";
import { ROLE } from "../../constants/DefaultConstants";
import PropTypes from "prop-types";
import { processTypeaheadOptions } from "./TypeaheadAnswer";
import { EXTENSIONS } from "../../../config";
import ExportRecordsDropdown from "./ExportRecordsDropdown";
import { isAdmin } from "../../utils/SecurityUtils";
import { hasRole, isAdmin } from "../../utils/SecurityUtils";
import ImportRecordsDialog from "./ImportRecordsDialog";
import PromiseTrackingMask from "../misc/PromiseTrackingMask";
import { trackPromise } from "react-promise-tracker";
Expand Down Expand Up @@ -57,7 +57,7 @@ class Records extends React.Component {
const showCreateButton = STUDY_CREATE_AT_MOST_ONE_RECORD
? !recordsLoaded.records || recordsLoaded.records.length < 1
: true;
const showPublishButton = isAdmin(this.props.currentUser) && EXTENSIONS === EXTENSION_CONSTANTS.OPERATOR;
const showPublishButton = isAdmin(this.props.currentUser) && hasRole(this.props.currentUser, ROLE.PUBLISH_RECORDS);
const createRecordDisabled = STUDY_CLOSED_FOR_ADDITION && !isAdmin(this.props.currentUser);
const createRecordTooltip = this.i18n(
createRecordDisabled ? "records.closed-study.create-tooltip" : "records.opened-study.create-tooltip",
Expand Down
5 changes: 3 additions & 2 deletions src/components/user/PasswordChangeController.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ROLE } from "../../constants/DefaultConstants";
import { changePassword } from "../../actions/UserActions";
import * as UserFactory from "../../utils/EntityFactory";
import PropTypes from "prop-types";
import { isAdmin } from "../../utils/SecurityUtils.js";

class PasswordChangeController extends React.Component {
constructor(props) {
Expand Down Expand Up @@ -51,7 +52,7 @@ class PasswordChangeController extends React.Component {

render() {
const { currentUser, passwordChange, match } = this.props;
if (!currentUser || (currentUser.role !== ROLE.ADMIN && currentUser.username !== match.params.username)) {
if (!currentUser || (!isAdmin(currentUser) && currentUser.username !== match.params.username)) {
return null;
}
const handlers = {
Expand Down Expand Up @@ -81,7 +82,7 @@ PasswordChangeController.propTypes = {
}).isRequired,
transitionToWithOpts: PropTypes.func.isRequired,
currentUser: PropTypes.shape({
role: PropTypes.string.isRequired,
roles: PropTypes.array.isRequired,
username: PropTypes.string.isRequired,
}).isRequired,
passwordChange: PropTypes.object.isRequired,
Expand Down
Loading
Loading