Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.

Permissions part 1 - permission based on Topcoder Roles #108

Merged
merged 2 commits into from
Feb 22, 2021
Merged
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
4 changes: 3 additions & 1 deletion src/components/ActionsMenu/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Shows dropdown menu with actions.
*/
import React, { useState, useCallback } from "react";
import _ from "lodash";
import PT from "prop-types";
import "./styles.module.scss";
import OutsideClickHandler from "react-outside-click-handler";
Expand Down Expand Up @@ -89,7 +90,7 @@ const ActionsMenu = ({ options = [] }) => {
{...attributes.popper}
>
<div styleName="list">
{options.map((option, index) => {
{_.reject(options, "hidden").map((option, index) => {
if (option.separator) {
return <div key={index} styleName="separator" />;
} else {
Expand Down Expand Up @@ -124,6 +125,7 @@ ActionsMenu.propTypes = {
label: PT.string,
action: PT.func,
separator: PT.bool,
hidden: PT.bool,
})
),
};
Expand Down
1 change: 1 addition & 0 deletions src/components/FormField/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const FormField = ({ field }) => {
onChange={input.onChange}
onBlur={input.onBlur}
onFocus={input.onFocus}
disabled={field.disabled}
/>
)}
{(field.isRequired || field.customValidator) &&
Expand Down
3 changes: 3 additions & 0 deletions src/components/ReactSelect/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const ReactSelect = (props) => {
onInputChange={props.onInputChange}
noOptionsMessage={() => props.noOptionsText}
createOptionPosition="first"
isDisabled={props.disabled}
/>
) : (
<Select
Expand All @@ -98,6 +99,7 @@ const ReactSelect = (props) => {
placeholder={props.placeholder}
onInputChange={props.onInputChange}
noOptionsMessage={() => props.noOptionsText}
isDisabled={props.disabled}
/>
)}
</div>
Expand All @@ -121,6 +123,7 @@ ReactSelect.propTypes = {
),
isCreatable: PT.bool,
noOptionsText: PT.string,
disabled: PT.bool,
};

export default ReactSelect;
24 changes: 16 additions & 8 deletions src/components/TCForm/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Shows form, field and actions.
*/
import React, { useState, useEffect } from "react";
import _ from "lodash";
import PT from "prop-types";
import { FORM_ROW_TYPE, FORM_FIELD_TYPE } from "../../constants";
import { Form } from "react-final-form";
Expand Down Expand Up @@ -56,17 +57,24 @@ const TCForm = ({
<>
{row.type === FORM_ROW_TYPE.GROUP && (
<div styleName="field-group">
{row.fields.map((field) => (
<div styleName="field-group-field" key={field.name}>
<FormField field={fields[field]} />
</div>
))}
{row.fields.map((field) =>
!fields[field].hidden ? (
<div
styleName="field-group-field"
key={field.name}
>
<FormField field={fields[field]} />
</div>
) : null
)}
</div>
)}
{row.type === FORM_ROW_TYPE.SINGLE &&
row.fields.map((field) => {
return <FormField field={fields[field]} />;
})}
row.fields.map((field) =>
!fields[field].hidden ? (
<FormField field={fields[field]} />
) : null
)}
</>
);
})}
Expand Down
101 changes: 101 additions & 0 deletions src/constants/permissions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* User permission policies.
* Can be used with `hasPermission` method.
*
* PERMISSION GUIDELINES
*
* All the permission name and meaning should define **WHAT** can be done having such permission
* but not **WHO** can do it.
*
* Examples of CORRECT permission naming and meaning:
* - `VIEW_PROJECT`
* - `EDIT_MILESTONE`
* - `DELETE_WORK`
*
* Examples of INCORRECT permissions naming and meaning:
* - `COPILOT_AND_MANAGER`
* - `PROJECT_MEMBERS`
* - `ADMINS`
*
* The same time **internally only** in this file, constants like `COPILOT_AND_ABOVE`,
* `PROJECT_MEMBERS`, `ADMINS` could be used to define permissions.
*
* NAMING GUIDELINES
*
* There are unified prefixes to indicate what kind of permissions.
* If no prefix is suitable, please, feel free to use a new prefix.
*
* CREATE_ - create somethings
* READ_ - read something
* UPDATE_ - update something
* DELETE_ - delete something
*
* MANAGE_ - means combination of 3 operations CREATE/UPDATE/DELETE.
* usually should be used, when READ operation is allowed to everyone
* while 3 manage operations require additional permissions
* ACCESS_ - means combination of all 4 operations READ/CREATE/UPDATE/DELETE.
* usually should be used, when by default users cannot even READ something
* and if someone can READ, then also can do other kind of operations.
*
* ANTI-PERMISSIONS
*
* If it's technically impossible to create permission rules for some situation in "allowed" manner,
* in such case we can create permission rules, which would disallow somethings.
* - Create such rules ONLY IF CREATING ALLOW RULE IS IMPOSSIBLE.
* - Add a comment to such rules explaining why allow-rule cannot be created.
*/
/* eslint-disable no-unused-vars */
import _ from "lodash";

/**
* Topcoder User roles
*
* Here we list only the part of the roles which might be used in the TaaS App
*/
export const TOPCODER_ROLE = {
BOOKING_MANAGER: "bookingmanager",
CONNECT_MANAGER: "Connect Manager",
ADMINISTRATOR: "administrator",
TOPCODER_USER: "Topcoder User",
};

/**
* Project Member roles
*
* NOTE: Team in the TaaS app is same as Project in Connect App or Projects Serivce API
*
* Here we list only the part of the project member roles which are used in TaaS App
*/
export const PROJECT_ROLE = {
CUSTOMER: "customer",
};

/*
* The next sets of roles are exported for outside usage with `hasPermission` method.
*/

export const PERMISSIONS = {
EDIT_RESOURCE_BOOKING: {
meta: {
group: "Resource Booking",
title: "Edit Resource Booking",
},
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},

ACCESS_RESOURCE_BOOKING_MEMBER_RATE: {
meta: {
group: "Resource Booking",
title: "Access Member Rate (view and edit)",
},
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},

EDIT_JOB_STATUS: {
meta: {
group: "Job",
title: 'Edit Job "status"',
},
topcoderRoles: [TOPCODER_ROLE.BOOKING_MANAGER, TOPCODER_ROLE.ADMINISTRATOR],
},
};
26 changes: 26 additions & 0 deletions src/hoc/withAuthentication/actions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/**
* Auth User actions
*/

export const ACTION_TYPE = {
AUTH_USER_SUCCESS: "AUTH_USER_SUCCESS",
AUTH_USER_ERROR: "AUTH_USER_ERROR",
};

/**
* Action to set auth user data
*
* @param {object} tokenData user data from token
*/
export const authUserSuccess = (tokenData) => ({
type: ACTION_TYPE.AUTH_USER_SUCCESS,
payload: tokenData,
});

/**
* Action to set auth user error
*/
export const authUserError = (error) => ({
type: ACTION_TYPE.AUTH_USER_ERROR,
payload: error,
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,38 @@
* wrap component for authentication
*/
import React, { useState, useEffect } from "react";
import _ from "lodash";
import { getAuthUserTokens, login } from "@topcoder/micro-frontends-navbar-app";
import LoadingIndicator from "../components/LoadingIndicator";
import LoadingIndicator from "../../components/LoadingIndicator";
import { authUserSuccess, authUserError } from "./actions";
import { decodeToken } from "utils/helpers";
import { useDispatch, useSelector } from "react-redux";

export default function withAuthentication(Component) {
const AuthenticatedComponent = (props) => {
let [isLoggedIn, setIsLoggedIn] = useState(null);
let [authError, setAuthError] = useState(false);
const dispatch = useDispatch();
const { isLoggedIn, authError } = useSelector((state) => state.authUser);

useEffect(() => {
// prevent page redirecting to login page when unmount
let isUnmount = false;
getAuthUserTokens()
.then(({ tokenV3 }) => {
if (!!tokenV3) {
setIsLoggedIn(!!tokenV3);
const tokenData = decodeToken(tokenV3);
dispatch(
authUserSuccess(_.pick(tokenData, ["userId", "handle", "roles"]))
);
} else if (!isUnmount) {
login();
}
})
.catch(setAuthError);
.catch((error) => dispatch(authUserError(error)));

return () => {
isUnmount = true;
};
}, []);
}, [dispatch]);

return (
<>
Expand Down
35 changes: 35 additions & 0 deletions src/hoc/withAuthentication/reducers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Reducer for `authUser`
*/

import { ACTION_TYPE } from "../actions";

const initialState = {
isLoggedIn: null,
userId: null,
handle: null,
roles: [],
authError: null,
};

const reducer = (state = initialState, action) => {
switch (action.type) {
case ACTION_TYPE.AUTH_USER_SUCCESS:
return {
...initialState,
...action.payload,
isLoggedIn: true,
};

case ACTION_TYPE.AUTH_USER_ERROR:
return {
...initialState,
authError: action.payload,
};

default:
return state;
}
};

export default reducer;
2 changes: 2 additions & 0 deletions src/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { reducer as toastrReducer } from "react-redux-toastr";
import positionDetailsReducer from "../routes/PositionDetails/reducers";
import teamMembersReducer from "../routes/TeamAccess/reducers";
import reportPopupReducer from "../components/ReportPopup/reducers";
import authUserReducer from "../hoc/withAuthentication/reducers";

const rootReducer = combineReducers({
toastr: toastrReducer,
positionDetails: positionDetailsReducer,
teamMembers: teamMembersReducer,
reportPopup: reportPopupReducer,
authUser: authUserReducer,
});

export default rootReducer;
3 changes: 3 additions & 0 deletions src/routes/JobForm/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* JobForm utilities
*/
import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";
import {
RATE_TYPE_OPTIONS,
STATUS_OPTIONS,
Expand Down Expand Up @@ -101,6 +103,7 @@ export const getEditJobConfig = (skillOptions, onSubmit) => {
validationMessage: "Please, select Status",
name: "status",
selectOptions: STATUS_OPTIONS,
disabled: !hasPermission(PERMISSIONS.EDIT_JOB_STATUS),
},
],
onSubmit: onSubmit,
Expand Down
4 changes: 4 additions & 0 deletions src/routes/MyTeamsDetails/components/TeamMembers/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
import Input from "components/Input";
import { skillShape } from "components/SkillsList";
import { useReportPopup } from "components/ReportPopup/hooks/useReportPopup";
import { hasPermission } from "utils/permissions";
import { PERMISSIONS } from "constants/permissions";

const TeamMembers = ({ team }) => {
const { resources, jobs } = team;
Expand Down Expand Up @@ -156,9 +158,11 @@ const TeamMembers = ({ team }) => {
`/taas/myteams/${team.id}/rb/${member.id}/edit`
);
},
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
},
{
separator: true,
hidden: !hasPermission(PERMISSIONS.EDIT_RESOURCE_BOOKING),
},
{
label: "Report an Issue",
Expand Down
14 changes: 9 additions & 5 deletions src/routes/ResourceBookingDetails/ResourceDetails/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import IconMoney from "../../../assets/images/icon-money.svg";
import IconComputer from "../../../assets/images/icon-computer.svg";
import IconBag from "../../../assets/images/icon-bag.svg";
import "./styles.module.scss";
import { PERMISSIONS } from "constants/permissions";
import { hasPermission } from "utils/permissions";

const ResourceDetails = ({ resource, jobTitle }) => {
return (
Expand Down Expand Up @@ -55,11 +57,13 @@ const ResourceDetails = ({ resource, jobTitle }) => {
/>
</div>
<div styleName="table-cell">
<DataItem
icon={<IconMoney />}
title="Member Rate"
children={formatMoney(resource.memberRate)}
/>
{hasPermission(PERMISSIONS.ACCESS_RESOURCE_BOOKING_MEMBER_RATE) && (
<DataItem
icon={<IconMoney />}
title="Member Rate"
children={formatMoney(resource.memberRate)}
/>
)}
</div>
</div>
</div>
Expand Down
Loading