Skip to content

Commit 899be6a

Browse files
maxceemMaksym Mykhailenko
and
Maksym Mykhailenko
authored
Support M2M and Unified permissions, part 1 (#555)
* feat: M2M support and Unified permissions, part 1 - Support for M2M operation and using unified permissions for CRUD /projects/{id} and CRUD /projects/{id}/members endpoints - Added script to generate Permissions Documentation - Fixed "initiatorUserId" and "userId" fields in 'connect.notification.project.team.updated' event payload - Allow directly create member by M2M and for admins - Fix: don't allow to change project members roles if such user doesn't have necessary Topcoder Roles - Fix: don't allow copilots to manage non-customer project members - * fix: ProjectEstimationsItems permissions config * chore: remove console.log * chore: remove console.log * refactor: unify endpoint permission names For project members and project member invites * feat: M2M support and Unified permissions, part 2 - Support for M2M operation and using unified permissions for CRUD /projects/{id}/invites endpoint - "generalPermission" middleware supports several permissions * feat: generate Roles Matrix in the HTML document * feat: roles matrix explains default role * fix: permission name * fix: permissions for Topcoder Team management * feat: invite status when member added directly When member is added directly "cancel" corresponding invite instead of "accept" it. Co-authored-by: Maksym Mykhailenko <maxcemm@gmail.com>
1 parent 9be8a21 commit 899be6a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+3028
-463
lines changed

config/default.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@
3939
"url": "localhost:9092"
4040
},
4141
"analyticsKey": "",
42-
"VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]",
43-
"validIssuers": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]",
42+
"VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]",
43+
"validIssuers": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]",
4444
"jwksUri": "",
4545
"busApiUrl": "http://api.topcoder-dev.com/v5",
4646
"messageApiUrl": "http://api.topcoder-dev.com/v5",

docs/permissions.html

Lines changed: 1299 additions & 0 deletions
Large diffs are not rendered by default.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
"data:export": "NODE_ENV=development LOG_LEVEL=info node --require dotenv/config --require babel-core/register scripts/data/export",
3131
"data:import": "NODE_ENV=development LOG_LEVEL=info node --require dotenv/config --require babel-core/register scripts/data/import",
3232
"local:run-docker": "docker-compose -f ./local/full/docker-compose.yml up -d",
33-
"local:init": "npm run sync:all && npm run data:import"
33+
"local:init": "npm run sync:all && npm run data:import",
34+
"generate:doc:permissions": "babel-node scripts/permissions-doc",
35+
"generate:doc:permissions:dev": "nodemon --watch scripts/permissions-doc --watch src --ext js,jsx,hbs --exec babel-node scripts/permissions-doc"
3436
},
3537
"repository": {
3638
"type": "git",

scripts/permissions-doc/index.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Generate a permissions.html document using the permission config from the Topcoder Connect App.
3+
*
4+
* Run by: `npm run generate:doc:permissions`
5+
*
6+
* For development purpose, run by `npm run generate:doc:permissions:dev` which would regenerate HTML on every update.
7+
*/
8+
import _ from 'lodash';
9+
import fs from 'fs';
10+
import path from 'path';
11+
import handlebars from 'handlebars';
12+
import {
13+
PERMISSION,
14+
PROJECT_TO_TOPCODER_ROLES_MATRIX,
15+
DEFAULT_PROJECT_ROLE,
16+
} from '../../src/permissions/constants';
17+
import {
18+
PROJECT_MEMBER_ROLE,
19+
} from '../../src/constants';
20+
import util from '../../src/util';
21+
22+
const docTemplatePath = path.resolve(__dirname, './template.hbs');
23+
const outputDocPath = path.resolve(__dirname, '../../docs/permissions.html');
24+
25+
handlebars.registerHelper('istrue', value => value === true);
26+
27+
/**
28+
* Normalize all the project and topcoder role lists to the list of strings.
29+
*
30+
* - `projectRoles` can be `true` -> full list of Project Roles
31+
* - `projectRoles` may contain an object for `owner` role -> `owner` (string)
32+
* - `topcoderRoles` can be `true` -> full list of Topcoder Roles
33+
*
34+
* @param {Object} rule permission rule
35+
*
36+
* @returns {Object} permission rule with all the roles as strings
37+
*/
38+
function normalizePermissionRule(rule) {
39+
const normalizedRule = _.cloneDeep(rule);
40+
41+
if (_.isArray(normalizedRule.projectRoles)) {
42+
normalizedRule.projectRoles = normalizedRule.projectRoles.map((role) => {
43+
if (_.isEqual(role, { role: PROJECT_MEMBER_ROLE.CUSTOMER, isPrimary: true })) {
44+
return 'owner';
45+
}
46+
47+
return role;
48+
});
49+
}
50+
51+
return normalizedRule;
52+
}
53+
54+
/**
55+
* Normalize permission object which has "simple" and "full" shape into a "full" shape for consistency
56+
*
57+
* @param {Object} permission permission object
58+
*
59+
* @returns {Objects} permission object in the "full" shape with "allowRule" and "denyRule"
60+
*/
61+
function normalizePermission(permission) {
62+
let normalizedPermission = permission;
63+
64+
if (!normalizedPermission.allowRule) {
65+
normalizedPermission = {
66+
meta: permission.meta,
67+
allowRule: _.omit(permission, 'meta'),
68+
};
69+
}
70+
71+
if (normalizedPermission.allowRule) {
72+
normalizedPermission.allowRule = normalizePermissionRule(normalizedPermission.allowRule);
73+
}
74+
75+
if (normalizedPermission.denyRule) {
76+
normalizedPermission.denyRule = normalizePermissionRule(normalizedPermission.denyRule);
77+
}
78+
79+
return normalizedPermission;
80+
}
81+
82+
/**
83+
* @returns {Object} project/topcoder roles matrix
84+
*/
85+
function getNormalizedRolesMatrix() {
86+
const topcoderRolesAll = _.values(_.map(DEFAULT_PROJECT_ROLE, 'topcoderRole'));
87+
const projectRolesAll = _.keys(PROJECT_TO_TOPCODER_ROLES_MATRIX);
88+
89+
const isDefaultRole = (topcoderRole, projectRole) =>
90+
util.getDefaultProjectRole({ roles: [topcoderRole] }) === projectRole;
91+
92+
const isAllowedRole = (topcoderRole, projectRole) =>
93+
(PROJECT_TO_TOPCODER_ROLES_MATRIX[projectRole] || []).includes(topcoderRole);
94+
95+
const columns = ['Project \\ Topcoder'].concat(topcoderRolesAll);
96+
const rows = projectRolesAll.map(projectRole => ({
97+
rowHeader: projectRole,
98+
cells: topcoderRolesAll.map(topcoderRole => ({
99+
isAllowed: isAllowedRole(topcoderRole, projectRole),
100+
isDefault: isDefaultRole(topcoderRole, projectRole),
101+
})),
102+
}));
103+
104+
// Uncomment if you want to switch columns and rows
105+
// const columns = ['Topcoder \\ Project'].concat(topcoderRolesAll);
106+
// const rows = topcoderRolesAll.map(topcoderRole => ({
107+
// rowHeader: topcoderRole,
108+
// cells: projectRolesAll.map(projectRole => ({
109+
// isAllowed: isAllowedRole(topcoderRole, projectRole),
110+
// isDefault: isDefaultRole(topcoderRole, projectRole),
111+
// })),
112+
// }));
113+
114+
return {
115+
columns,
116+
rows,
117+
};
118+
}
119+
120+
const templateStr = fs.readFileSync(docTemplatePath).toString();
121+
const renderDocument = handlebars.compile(templateStr);
122+
123+
const permissionKeys = _.keys(PERMISSION);
124+
// prepare permissions without modifying data in constant `PERMISSION`
125+
const allPermissions = permissionKeys.map((key) => {
126+
// add `key` to meta
127+
const meta = _.assign({}, PERMISSION[key].meta, {
128+
key,
129+
});
130+
131+
// update `meta` to one with `key`
132+
return _.assign({}, PERMISSION[key], {
133+
meta,
134+
});
135+
});
136+
const groupsObj = _.groupBy(allPermissions, 'meta.group');
137+
const groups = _.toPairs(groupsObj).map(([title, permissions]) => ({
138+
title,
139+
anchor: `section-${title.toLowerCase().replace(' ', '-')}`,
140+
permissions,
141+
}));
142+
143+
groups.forEach((group) => {
144+
group.permissions = group.permissions.map(normalizePermission); // eslint-disable-line no-param-reassign
145+
});
146+
147+
const data = {
148+
groups,
149+
rolesMatrix: getNormalizedRolesMatrix(),
150+
};
151+
152+
fs.writeFileSync(outputDocPath, renderDocument(data));

scripts/permissions-doc/template.hbs

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
7+
<title>Permissions</title>
8+
<style>
9+
.small-text {
10+
font-size: 62.5%;
11+
line-height: 120%;
12+
}
13+
14+
.permission-variable {
15+
line-height: 80%;
16+
margin: 5px 0;
17+
word-break: break-word;
18+
}
19+
20+
.permission-title {
21+
line-height: 120%;
22+
margin: 5px 0;
23+
}
24+
25+
.anchor-container .anchor {
26+
float: left;
27+
opacity: 1;
28+
line-height: 1;
29+
margin-left: -20px;
30+
}
31+
32+
.anchor-container .anchor::before {
33+
visibility: hidden;
34+
font-size: 16px;
35+
content: '🔗';
36+
display: inline-block;
37+
width: 20px;
38+
}
39+
40+
.anchor-container:hover .anchor {
41+
text-decoration: none;
42+
}
43+
44+
.anchor-container:hover .anchor::before {
45+
visibility: visible;
46+
}
47+
48+
.roles-matrix-table td {
49+
vertical-align: center;
50+
text-align: center;
51+
}
52+
53+
.roles-matrix-table thead th:not(:first-child),
54+
.roles-matrix-table td {
55+
border-left: 1px solid #dee2e6;
56+
}
57+
58+
.roles-matrix-table thead th {
59+
height: 320px;
60+
}
61+
62+
.roles-matrix-table thead th:not(:first-child) {
63+
width: 40px;
64+
}
65+
66+
.roles-matrix-table thead th div {
67+
white-space: nowrap;
68+
}
69+
70+
.roles-matrix-table thead th:not(:first-child) div {
71+
padding-left: 10px;
72+
transform: rotate(-90deg);
73+
width: 40px;
74+
}
75+
76+
.roles-matrix-table tfoot td {
77+
border-left: 0;
78+
text-align: left;
79+
}
80+
81+
.roles-matrix-default-color {
82+
width: 15px;
83+
height: 15px;
84+
display: inline-block;
85+
background-color: #b8daff;
86+
vertical-align: middle;
87+
}
88+
</style>
89+
</head>
90+
<body>
91+
<div class="container">
92+
<div class="jumbotron">
93+
<h1 class="display-4">Permissions</h1>
94+
<p class="lead">List of all the possible user permissions inside Topcoder Project Service</p>
95+
<hr class="my-4">
96+
<p>Legend:</p>
97+
<ul>
98+
<li><span class="badge badge-primary">allowed Project Role</span> - users with such a <strong>Project Role</strong> are allowed to perform the action</li>
99+
<li><span class="badge badge-warning">denied Project Role</span> - users with such a <strong>Project Role</strong> are denied to perform the action even they have some other allow roles</li>
100+
<li><span class="badge badge-success">allowed Topcoder Role</span> - users with such a <strong>Topcoder Role</strong> are allowed to perform the action</li>
101+
<li><span class="badge badge-danger">denied Topcoder Role</span> - users with such a <strong>Topcoder Role</strong> are denied to perform the action even they have some other allow roles</li>
102+
<li><span class="badge badge-dark">allowed M2M Scope</span> - M2M tokens with such a <strong>scope</strong> are allowed to perform the action</li>
103+
<li><span class="badge badge-secondary">denied M2M Scope</span> - M2M tokens with such a <strong>scope</strong> are allowed to perform the action even they have some other allow scopes</li>
104+
</ul>
105+
</div>
106+
107+
{{#each groups}}
108+
<div class="row">
109+
<div class="col pt-5 pb-2">
110+
<h2 class="anchor-container">
111+
<a href="#{{anchor}}" name="{{anchor}}" class="anchor"></a>{{title}}
112+
</h2>
113+
</div>
114+
</div>
115+
{{#each permissions}}
116+
<div class="row border-top">
117+
<div class="col py-2">
118+
<div class="permission-title anchor-container">
119+
<a href="#{{meta.key}}" name="{{meta.key}}" class="anchor"></a>{{meta.title}}
120+
</div>
121+
<div class="permission-variable"><small><code>{{meta.key}}</code></small></div>
122+
<div class="text-black-50 small-text">{{meta.description}}</div>
123+
</div>
124+
<div class="col-9 py-2">
125+
<div>
126+
{{#if (istrue allowRule.projectRoles)}}
127+
<span class="badge badge-primary" title="Allowed">Any Project Member</span>
128+
{{else}}
129+
{{#each allowRule.projectRoles}}
130+
<span class="badge badge-primary" title="Allowed Project Role">{{this}}</span>
131+
{{/each}}
132+
{{/if}}
133+
{{#each denyRule.projectRoles}}
134+
<span class="badge badge-warning" title="Denied Project Role">{{this}}</span>
135+
{{/each}}
136+
</div>
137+
138+
<div>
139+
{{#if (istrue allowRule.topcoderRoles)}}
140+
<span class="badge badge-success" title="Allowed">Any Logged-in User</span>
141+
{{else}}
142+
{{#each allowRule.topcoderRoles}}
143+
<span class="badge badge-success" title="Allowed Topcoder Role">{{this}}</span>
144+
{{/each}}
145+
{{/if}}
146+
{{#each denyRule.topcoderRoles}}
147+
<span class="badge badge-danger" title="Denied Topcoder Role">{{this}}</span>
148+
{{/each}}
149+
</div>
150+
151+
<div>
152+
{{#each allowRule.scopes}}
153+
<span class="badge badge-dark" title="Allowed Topcoder Role">{{this}}</span>
154+
{{/each}}
155+
{{#each denyRule.scopes}}
156+
<span class="badge badge-secondary" title="Denied Topcoder Role">{{this}}</span>
157+
{{/each}}
158+
</div>
159+
</div>
160+
</div>
161+
{{/each}}
162+
{{/each}}
163+
164+
<h1 class="anchor-container">
165+
<a href="#roles-matrix" name="roles-matrix" class="anchor"></a>Roles Matrix
166+
</h1>
167+
168+
<table class="table table-hover roles-matrix-table">
169+
<thead>
170+
<tr>
171+
{{#each rolesMatrix.columns}}
172+
<th><div>{{this}}</div></th>
173+
{{/each}}
174+
</tr>
175+
</thead>
176+
<tbody>
177+
{{#each rolesMatrix.rows}}
178+
<tr>
179+
<th>{{this.rowHeader}}</th>
180+
{{#each this.cells}}
181+
<td {{#if this.isDefault}}class="table-primary" title="Default role"{{/if}}>
182+
{{#if this.isAllowed}}{{/if}}
183+
</td>
184+
{{/each}}
185+
</tr>
186+
{{/each}}
187+
</tbody>
188+
<tfoot>
189+
<tr>
190+
<td colspan="{{rolesMatrix.columns.length}}">
191+
<div class="roles-matrix-default-color"></div> - means default <strong>Project Role</strong> if user with according <strong>Topcoder Role</strong> directly joins the project (if they are allowed to join directly). If user has multiple <strong>Topcoder Roles</strong> then the most left <strong>Topcoder Role</strong> on the table would define default <strong>Project Role</strong>.
192+
</td>
193+
</tr>
194+
</tfoot>
195+
</table>
196+
</div>
197+
</body>
198+
</html>

0 commit comments

Comments
 (0)