Skip to content

Support M2M and Unified permissions, part 1 #555

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

Merged
merged 11 commits into from
Apr 28, 2020
Merged
Show file tree
Hide file tree
Changes from 10 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: 2 additions & 2 deletions config/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@
"url": "localhost:9092"
},
"analyticsKey": "",
"VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]",
"validIssuers": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\"]",
"VALID_ISSUERS": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]",
"validIssuers": "[\"https:\/\/topcoder-newauth.auth0.com\/\",\"https:\/\/api.topcoder-dev.com\",\"https:\/\/topcoder-dev.auth0.com\/\"]",
"jwksUri": "",
"busApiUrl": "http://api.topcoder-dev.com/v5",
"messageApiUrl": "http://api.topcoder-dev.com/v5",
Expand Down
1,299 changes: 1,299 additions & 0 deletions docs/permissions.html

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
"data:export": "NODE_ENV=development LOG_LEVEL=info node --require dotenv/config --require babel-core/register scripts/data/export",
"data:import": "NODE_ENV=development LOG_LEVEL=info node --require dotenv/config --require babel-core/register scripts/data/import",
"local:run-docker": "docker-compose -f ./local/full/docker-compose.yml up -d",
"local:init": "npm run sync:all && npm run data:import"
"local:init": "npm run sync:all && npm run data:import",
"generate:doc:permissions": "babel-node scripts/permissions-doc",
"generate:doc:permissions:dev": "nodemon --watch scripts/permissions-doc --watch src --ext js,jsx,hbs --exec babel-node scripts/permissions-doc"
},
"repository": {
"type": "git",
Expand Down
152 changes: 152 additions & 0 deletions scripts/permissions-doc/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* Generate a permissions.html document using the permission config from the Topcoder Connect App.
*
* Run by: `npm run generate:doc:permissions`
*
* For development purpose, run by `npm run generate:doc:permissions:dev` which would regenerate HTML on every update.
*/
import _ from 'lodash';
import fs from 'fs';
import path from 'path';
import handlebars from 'handlebars';
import {
PERMISSION,
PROJECT_TO_TOPCODER_ROLES_MATRIX,
DEFAULT_PROJECT_ROLE,
} from '../../src/permissions/constants';
import {
PROJECT_MEMBER_ROLE,
} from '../../src/constants';
import util from '../../src/util';

const docTemplatePath = path.resolve(__dirname, './template.hbs');
const outputDocPath = path.resolve(__dirname, '../../docs/permissions.html');

handlebars.registerHelper('istrue', value => value === true);

/**
* Normalize all the project and topcoder role lists to the list of strings.
*
* - `projectRoles` can be `true` -> full list of Project Roles
* - `projectRoles` may contain an object for `owner` role -> `owner` (string)
* - `topcoderRoles` can be `true` -> full list of Topcoder Roles
*
* @param {Object} rule permission rule
*
* @returns {Object} permission rule with all the roles as strings
*/
function normalizePermissionRule(rule) {
const normalizedRule = _.cloneDeep(rule);

if (_.isArray(normalizedRule.projectRoles)) {
normalizedRule.projectRoles = normalizedRule.projectRoles.map((role) => {
if (_.isEqual(role, { role: PROJECT_MEMBER_ROLE.CUSTOMER, isPrimary: true })) {
return 'owner';
}

return role;
});
}

return normalizedRule;
}

/**
* Normalize permission object which has "simple" and "full" shape into a "full" shape for consistency
*
* @param {Object} permission permission object
*
* @returns {Objects} permission object in the "full" shape with "allowRule" and "denyRule"
*/
function normalizePermission(permission) {
let normalizedPermission = permission;

if (!normalizedPermission.allowRule) {
normalizedPermission = {
meta: permission.meta,
allowRule: _.omit(permission, 'meta'),
};
}

if (normalizedPermission.allowRule) {
normalizedPermission.allowRule = normalizePermissionRule(normalizedPermission.allowRule);
}

if (normalizedPermission.denyRule) {
normalizedPermission.denyRule = normalizePermissionRule(normalizedPermission.denyRule);
}

return normalizedPermission;
}

/**
* @returns {Object} project/topcoder roles matrix
*/
function getNormalizedRolesMatrix() {
const topcoderRolesAll = _.values(_.map(DEFAULT_PROJECT_ROLE, 'topcoderRole'));
const projectRolesAll = _.keys(PROJECT_TO_TOPCODER_ROLES_MATRIX);

const isDefaultRole = (topcoderRole, projectRole) =>
util.getDefaultProjectRole({ roles: [topcoderRole] }) === projectRole;

const isAllowedRole = (topcoderRole, projectRole) =>
(PROJECT_TO_TOPCODER_ROLES_MATRIX[projectRole] || []).includes(topcoderRole);

const columns = ['Project \\ Topcoder'].concat(topcoderRolesAll);
const rows = projectRolesAll.map(projectRole => ({
rowHeader: projectRole,
cells: topcoderRolesAll.map(topcoderRole => ({
isAllowed: isAllowedRole(topcoderRole, projectRole),
isDefault: isDefaultRole(topcoderRole, projectRole),
})),
}));

// Uncomment if you want to switch columns and rows
// const columns = ['Topcoder \\ Project'].concat(topcoderRolesAll);
// const rows = topcoderRolesAll.map(topcoderRole => ({
// rowHeader: topcoderRole,
// cells: projectRolesAll.map(projectRole => ({
// isAllowed: isAllowedRole(topcoderRole, projectRole),
// isDefault: isDefaultRole(topcoderRole, projectRole),
// })),
// }));

return {
columns,
rows,
};
}

const templateStr = fs.readFileSync(docTemplatePath).toString();
const renderDocument = handlebars.compile(templateStr);

const permissionKeys = _.keys(PERMISSION);
// prepare permissions without modifying data in constant `PERMISSION`
const allPermissions = permissionKeys.map((key) => {
// add `key` to meta
const meta = _.assign({}, PERMISSION[key].meta, {
key,
});

// update `meta` to one with `key`
return _.assign({}, PERMISSION[key], {
meta,
});
});
const groupsObj = _.groupBy(allPermissions, 'meta.group');
const groups = _.toPairs(groupsObj).map(([title, permissions]) => ({
title,
anchor: `section-${title.toLowerCase().replace(' ', '-')}`,
permissions,
}));

groups.forEach((group) => {
group.permissions = group.permissions.map(normalizePermission); // eslint-disable-line no-param-reassign
});

const data = {
groups,
rolesMatrix: getNormalizedRolesMatrix(),
};

fs.writeFileSync(outputDocPath, renderDocument(data));
198 changes: 198 additions & 0 deletions scripts/permissions-doc/template.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<title>Permissions</title>
<style>
.small-text {
font-size: 62.5%;
line-height: 120%;
}

.permission-variable {
line-height: 80%;
margin: 5px 0;
word-break: break-word;
}

.permission-title {
line-height: 120%;
margin: 5px 0;
}

.anchor-container .anchor {
float: left;
opacity: 1;
line-height: 1;
margin-left: -20px;
}

.anchor-container .anchor::before {
visibility: hidden;
font-size: 16px;
content: '🔗';
display: inline-block;
width: 20px;
}

.anchor-container:hover .anchor {
text-decoration: none;
}

.anchor-container:hover .anchor::before {
visibility: visible;
}

.roles-matrix-table td {
vertical-align: center;
text-align: center;
}

.roles-matrix-table thead th:not(:first-child),
.roles-matrix-table td {
border-left: 1px solid #dee2e6;
}

.roles-matrix-table thead th {
height: 320px;
}

.roles-matrix-table thead th:not(:first-child) {
width: 40px;
}

.roles-matrix-table thead th div {
white-space: nowrap;
}

.roles-matrix-table thead th:not(:first-child) div {
padding-left: 10px;
transform: rotate(-90deg);
width: 40px;
}

.roles-matrix-table tfoot td {
border-left: 0;
text-align: left;
}

.roles-matrix-default-color {
width: 15px;
height: 15px;
display: inline-block;
background-color: #b8daff;
vertical-align: middle;
}
</style>
</head>
<body>
<div class="container">
<div class="jumbotron">
<h1 class="display-4">Permissions</h1>
<p class="lead">List of all the possible user permissions inside Topcoder Project Service</p>
<hr class="my-4">
<p>Legend:</p>
<ul>
<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>
<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>
<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>
<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>
<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>
<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>
</ul>
</div>

{{#each groups}}
<div class="row">
<div class="col pt-5 pb-2">
<h2 class="anchor-container">
<a href="#{{anchor}}" name="{{anchor}}" class="anchor"></a>{{title}}
</h2>
</div>
</div>
{{#each permissions}}
<div class="row border-top">
<div class="col py-2">
<div class="permission-title anchor-container">
<a href="#{{meta.key}}" name="{{meta.key}}" class="anchor"></a>{{meta.title}}
</div>
<div class="permission-variable"><small><code>{{meta.key}}</code></small></div>
<div class="text-black-50 small-text">{{meta.description}}</div>
</div>
<div class="col-9 py-2">
<div>
{{#if (istrue allowRule.projectRoles)}}
<span class="badge badge-primary" title="Allowed">Any Project Member</span>
{{else}}
{{#each allowRule.projectRoles}}
<span class="badge badge-primary" title="Allowed Project Role">{{this}}</span>
{{/each}}
{{/if}}
{{#each denyRule.projectRoles}}
<span class="badge badge-warning" title="Denied Project Role">{{this}}</span>
{{/each}}
</div>

<div>
{{#if (istrue allowRule.topcoderRoles)}}
<span class="badge badge-success" title="Allowed">Any Logged-in User</span>
{{else}}
{{#each allowRule.topcoderRoles}}
<span class="badge badge-success" title="Allowed Topcoder Role">{{this}}</span>
{{/each}}
{{/if}}
{{#each denyRule.topcoderRoles}}
<span class="badge badge-danger" title="Denied Topcoder Role">{{this}}</span>
{{/each}}
</div>

<div>
{{#each allowRule.scopes}}
<span class="badge badge-dark" title="Allowed Topcoder Role">{{this}}</span>
{{/each}}
{{#each denyRule.scopes}}
<span class="badge badge-secondary" title="Denied Topcoder Role">{{this}}</span>
{{/each}}
</div>
</div>
</div>
{{/each}}
{{/each}}

<h1 class="anchor-container">
<a href="#roles-matrix" name="roles-matrix" class="anchor"></a>Roles Matrix
</h1>

<table class="table table-hover roles-matrix-table">
<thead>
<tr>
{{#each rolesMatrix.columns}}
<th><div>{{this}}</div></th>
{{/each}}
</tr>
</thead>
<tbody>
{{#each rolesMatrix.rows}}
<tr>
<th>{{this.rowHeader}}</th>
{{#each this.cells}}
<td {{#if this.isDefault}}class="table-primary" title="Default role"{{/if}}>
{{#if this.isAllowed}}✅{{/if}}
</td>
{{/each}}
</tr>
{{/each}}
</tbody>
<tfoot>
<tr>
<td colspan="{{rolesMatrix.columns.length}}">
<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>.
</td>
</tr>
</tfoot>
</table>
</div>
</body>
</html>
Loading