Skip to content

Commit

Permalink
Merge pull request #570: Add RESTful API endpoints for Groups customi…
Browse files Browse the repository at this point in the history
…zations
  • Loading branch information
victorlin committed Sep 28, 2022
2 parents d35785e + 6b48bc9 commit 73b0ca2
Show file tree
Hide file tree
Showing 13 changed files with 3,166 additions and 3,888 deletions.
5 changes: 2 additions & 3 deletions docs/authz.rst
Expand Up @@ -26,7 +26,7 @@ Policies

There is no single policy for all of nextstrain.org but different policies for
different parts of the site. Currently, policies are defined for and attached
to each :term:`Source`.
to each :term:`Source` and :term:`Group`.

The design of the system allows for policies to be easily stacked or combined
(e.g. concatenate all the rules), so if necessary we could introduce a global
Expand Down Expand Up @@ -126,8 +126,7 @@ The main enforcement function used to guard access-controlled code is::
It throws an ``AuthzDenied`` exception if the *user* is **not** allowed to
perform the *action* on the *object* as determined by the policy covering the
object (i.e. from the object's ``Source`` currently). Otherwise, it returns
nothing.
object (e.g. from the object's ``Source``). Otherwise, it returns nothing.

It is the responsibility of the enforcement function to determine the policy in
force for the given object.
6,125 changes: 2,558 additions & 3,567 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -23,7 +23,7 @@
},
"dependencies": {
"@aws-crypto/client-node": "^3.1.1",
"@aws-sdk/client-cognito-identity-provider": "^3.52.0",
"@aws-sdk/client-cognito-identity-provider": "^3.53.0",
"@aws-sdk/client-iam": "^3.53.0",
"@aws-sdk/client-s3": "^3.53.1",
"argparse": "^1.0.10",
Expand Down Expand Up @@ -78,7 +78,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"http-proxy": "^1.18.1",
"http-proxy-middleware": "^1.3.1",
"jest": "^26.4.2",
"jest": "^27.5.1",
"jest-extended": "^1.1.0",
"request": "^2.88.2",
"start-server-and-test": "^1.11.4"
Expand Down
27 changes: 27 additions & 0 deletions src/app.js
Expand Up @@ -42,6 +42,15 @@ const {
deleteNarrative,
} = endpoints.sources;

const {
getGroupLogo,
putGroupLogo,
deleteGroupLogo,
getGroupOverview,
putGroupOverview,
deleteGroupOverview,
} = endpoints.groups;

const {
CoreSource,
CoreStagingSource,
Expand Down Expand Up @@ -302,6 +311,24 @@ app.routeAsync("/groups/:groupName")
.getAsync(endpoints.static.sendGatsbyEntrypoint)
;

app.use("/groups/:groupName/settings",
endpoints.groups.setGroup(req => req.params.groupName));

app.routeAsync("/groups/:groupName/settings/logo")
.getAsync(getGroupLogo)
.putAsync(putGroupLogo)
.deleteAsync(deleteGroupLogo)
;

app.routeAsync("/groups/:groupName/settings/overview")
.getAsync(getGroupOverview)
.putAsync(putGroupOverview)
.deleteAsync(deleteGroupOverview)
;

app.route("/groups/:groupName/settings/*")
.all(() => { throw new NotFound(); });

// Avoid matching "narratives" as a dataset name.
app.routeAsync("/groups/:groupName/narratives")
.getAsync((req, res) => res.redirect(`/groups/${esc(req.params.groupName)}`));
Expand Down
2 changes: 2 additions & 0 deletions src/authz/index.js
@@ -1,5 +1,6 @@
import { strict as assert } from 'assert';
import { AuthzDenied } from '../exceptions.js';
import { Group } from "../groups.js";
import { Source, Resource } from '../sources/models.js';
import actions from './actions.js';
import tags from './tags.js';
Expand Down Expand Up @@ -49,6 +50,7 @@ const authorized = (user, action, object) => {
*/
/* eslint-disable indent, no-multi-spaces, semi-spacing */
const policy =
object instanceof Group ? object.authzPolicy :
object instanceof Source ? object.authzPolicy :
object instanceof Resource ? object.source.authzPolicy :
null ;
Expand Down
1 change: 1 addition & 0 deletions src/authz/tags.js
Expand Up @@ -14,6 +14,7 @@
*/
const tags = {
Type: {
Group: Symbol("type:group"),
Source: Symbol("type:source"),
Dataset: Symbol("type:dataset"),
Narrative: Symbol("type:narrative"),
Expand Down
160 changes: 160 additions & 0 deletions src/endpoints/groups.js
@@ -0,0 +1,160 @@
import * as authz from "../authz/index.js";
import { Group } from "../groups.js";
import {contentTypesProvided, contentTypesConsumed} from "../negotiate.js";
import {deleteByUrls, proxyFromUpstream, proxyToUpstream} from "../upstream.js";


const setGroup = (nameExtractor) => (req, res, next) => {
const group = new Group(nameExtractor(req));

authz.assertAuthorized(req.user, authz.actions.Read, group);

req.context.group = group;
return next();
};


/* Group customizations
*/


/* Group logo
*/


/* GET
*/
const getGroupLogo = contentTypesProvided([
["image/png", sendGroupLogo],
]);


/* PUT
*/
const putGroupLogo = contentTypesConsumed([
["image/png", receiveGroupLogo],
]);


/* DELETE
*/
const deleteGroupLogo = async (req, res) => {
authz.assertAuthorized(req.user, authz.actions.Write, req.context.group);

const method = "DELETE";
const url = await req.context.group.source.urlFor("group-logo.png", method);
await deleteByUrls([url]);

return res.status(204).end();
};


/* Group overview
*/


/* GET
*/
const getGroupOverview = contentTypesProvided([
["text/markdown", sendGroupOverview],
["text/plain", sendGroupOverview],
]);


/* PUT
*/
const putGroupOverview = contentTypesConsumed([
["text/markdown", receiveGroupOverview],
]);


/* DELETE
*/
const deleteGroupOverview = async (req, res) => {
authz.assertAuthorized(req.user, authz.actions.Write, req.context.group);

const method = "DELETE";
const url = await req.context.group.source.urlFor("group-overview.md", method);
await deleteByUrls([url]);

return res.status(204).end();
};


/**
* An Express endpoint that sends a group overview determined by the request.
*
* @param {express.request} req - Express-style request instance
* @param {express.response} res - Express-style response instance
* @returns {expressEndpointAsync}
*/
async function sendGroupOverview(req, res) {
authz.assertAuthorized(req.user, authz.actions.Read, req.context.group);

return await proxyFromUpstream(req, res,
await req.context.group.source.urlFor("group-overview.md"),
"text/markdown"
);
}


/**
* An Express endpoint that receives a group overview determined by the request.
*
* @param {express.request} req - Express-style request instance
* @param {express.response} res - Express-style response instance
* @returns {expressEndpointAsync}
*/
async function receiveGroupOverview(req, res) {
authz.assertAuthorized(req.user, authz.actions.Write, req.context.group);

return await proxyToUpstream(req, res,
async (method, headers) => await req.context.group.source.urlFor("group-overview.md", method, headers),
"text/markdown"
);
}


/**
* An Express endpoint that sends a group logo determined by the request.
*
* @param {express.request} req - Express-style request instance
* @param {express.response} res - Express-style response instance
* @returns {expressEndpointAsync}
*/
async function sendGroupLogo(req, res) {
authz.assertAuthorized(req.user, authz.actions.Read, req.context.group);

return await proxyFromUpstream(req, res,
await req.context.group.source.urlFor("group-logo.png"),
"image/png"
);
}


/**
* An Express endpoint that receives a group logo determined by the request.
*
* @param {express.request} req - Express-style request instance
* @param {express.response} res - Express-style response instance
* @returns {expressEndpointAsync}
*/
async function receiveGroupLogo(req, res) {
authz.assertAuthorized(req.user, authz.actions.Write, req.context.group);

return await proxyToUpstream(req, res,
async (method, headers) => await req.context.group.source.urlFor("group-logo.png", method, headers),
"image/png"
);
}


export {
setGroup,
getGroupLogo,
putGroupLogo,
deleteGroupLogo,
getGroupOverview,
putGroupOverview,
deleteGroupOverview,
};
2 changes: 2 additions & 0 deletions src/endpoints/index.js
@@ -1,12 +1,14 @@
import * as charon from './charon/index.js';
import * as cli from './cli.js';
import * as groups from "./groups.js";
import * as sources from './sources.js';
import * as static_ from './static.js';
import * as users from './users.js';

export {
charon,
cli,
groups,
sources,
static_ as static,
users,
Expand Down

0 comments on commit 73b0ca2

Please sign in to comment.