Skip to content

Commit

Permalink
Merge branch 'trs/generalize-options'
Browse files Browse the repository at this point in the history
  • Loading branch information
tsibley committed Jul 5, 2023
2 parents 5b68084 + bb83c4b commit 64beb5a
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 43 deletions.
57 changes: 31 additions & 26 deletions docs/api-restful.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ Authentication
Authentication is required for:

1. All resource-modifying requests (PUT, DELETE).
2. Read-only requests (GET, HEAD) to private resources (e.g. private `Nextstrain
Groups`_).
2. Read-only requests (GET, HEAD, OPTIONS) to private resources (e.g. private
`Nextstrain Groups`_).
3. All requests to group settings endpoints.

Authentication is not required to download public datasets or narratives.
Expand Down Expand Up @@ -232,6 +232,11 @@ DELETE
Removes all representations of the resource identified by the request URL.
Responds with status 204 if successful.

OPTIONS
Lists the methods, via the ``Allow`` header, that the authenticated user
(if any) is authorized to use on the resource identified by the request
URL. Responds with status 204 is successful.


Conditional requests
====================
Expand All @@ -248,45 +253,45 @@ Endpoints

The following dataset endpoints exist::

{GET, HEAD, PUT, DELETE} /dengue/*
{GET, HEAD, PUT, DELETE} /ebola/*
{GET, HEAD, PUT, DELETE} /enterovirus/*
{GET, HEAD, PUT, DELETE} /flu/*
{GET, HEAD, PUT, DELETE} /lassa/*
{GET, HEAD, PUT, DELETE} /measles/*
{GET, HEAD, PUT, DELETE} /mers/*
{GET, HEAD, PUT, DELETE} /mumps/*
{GET, HEAD, PUT, DELETE} /ncov/*
{GET, HEAD, PUT, DELETE} /tb/*
{GET, HEAD, PUT, DELETE} /WNV/*
{GET, HEAD, PUT, DELETE} /yellow-fever/*
{GET, HEAD, PUT, DELETE} /zika/*
{GET, HEAD, PUT, DELETE, OPTIONS} /dengue/*
{GET, HEAD, PUT, DELETE, OPTIONS} /ebola/*
{GET, HEAD, PUT, DELETE, OPTIONS} /enterovirus/*
{GET, HEAD, PUT, DELETE, OPTIONS} /flu/*
{GET, HEAD, PUT, DELETE, OPTIONS} /lassa/*
{GET, HEAD, PUT, DELETE, OPTIONS} /measles/*
{GET, HEAD, PUT, DELETE, OPTIONS} /mers/*
{GET, HEAD, PUT, DELETE, OPTIONS} /mumps/*
{GET, HEAD, PUT, DELETE, OPTIONS} /ncov/*
{GET, HEAD, PUT, DELETE, OPTIONS} /tb/*
{GET, HEAD, PUT, DELETE, OPTIONS} /WNV/*
{GET, HEAD, PUT, DELETE, OPTIONS} /yellow-fever/*
{GET, HEAD, PUT, DELETE, OPTIONS} /zika/*

{GET, HEAD, PUT, DELETE} /staging/*
{GET, HEAD, PUT, DELETE, OPTIONS} /staging/*

{GET, HEAD, PUT, DELETE} /groups/{name}/*
{GET, HEAD, PUT, DELETE, OPTIONS} /groups/{name}/*

{GET, HEAD} /community/{user}/{repo}/*
{GET, HEAD, OPTIONS} /community/{user}/{repo}/*

{GET, HEAD} /fetch/*
{GET, HEAD, OPTIONS} /fetch/*

The following narrative endpoints exist::

{GET, HEAD, PUT, DELETE} /narratives/*
{GET, HEAD, PUT, DELETE, OPTIONS} /narratives/*

{GET, HEAD, PUT, DELETE} /staging/narratives/*
{GET, HEAD, PUT, DELETE, OPTIONS} /staging/narratives/*

{GET, HEAD, PUT, DELETE} /groups/{name}/narratives/*
{GET, HEAD, PUT, DELETE, OPTIONS} /groups/{name}/narratives/*

{GET, HEAD} /community/narratives/{user}/{repo}/*
{GET, HEAD, OPTIONS} /community/narratives/{user}/{repo}/*

{GET, HEAD} /fetch/narratives/*
{GET, HEAD, OPTIONS} /fetch/narratives/*

The following group settings endpoints exist::

{GET, HEAD, PUT, DELETE} /groups/{name}/settings/logo
{GET, HEAD, PUT, DELETE, OPTIONS} /groups/{name}/settings/logo

{GET, HEAD, PUT, DELETE} /groups/{name}/settings/overview
{GET, HEAD, PUT, DELETE, OPTIONS} /groups/{name}/settings/overview

.. _motivation:

Expand Down
18 changes: 15 additions & 3 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,15 @@ const {
getDataset,
putDataset,
deleteDataset,
optionsDataset,
getNarrative,
putNarrative,
deleteNarrative,
optionsNarrative,
} = endpoints.sources;

const {
optionsGroupSettings,
optionsGroup,
getGroupLogo,
putGroupLogo,
deleteGroupLogo,
Expand Down Expand Up @@ -212,13 +214,15 @@ app.routeAsync(coreBuildRoutes)
.getAsync(getDataset)
.putAsync(putDataset)
.deleteAsync(deleteDataset)
.optionsAsync(optionsDataset)
;

app.routeAsync("/narratives/*")
.all(setNarrative(req => req.params[0]))
.getAsync(getNarrative)
.putAsync(putNarrative)
.deleteAsync(deleteNarrative)
.optionsAsync(optionsNarrative)
;


Expand All @@ -238,13 +242,15 @@ app.routeAsync("/staging/narratives/*")
.getAsync(getNarrative)
.putAsync(putNarrative)
.deleteAsync(deleteNarrative)
.optionsAsync(optionsNarrative)
;

app.routeAsync("/staging/*")
.all(setDataset(req => req.params[0]), canonicalizeDataset(path => `/staging/${path}`))
.getAsync(getDataset)
.putAsync(putDataset)
.deleteAsync(deleteDataset)
.optionsAsync(optionsDataset)
;


Expand All @@ -268,11 +274,13 @@ app.use(["/community/narratives/:user/:repo", "/community/:user/:repo"],
app.routeAsync(["/community/narratives/:user/:repo", "/community/narratives/:user/:repo/*"])
.all(setNarrative(req => req.params[0]))
.getAsync(getNarrative)
.optionsAsync(optionsNarrative)
;

app.routeAsync(["/community/:user/:repo", "/community/:user/:repo/*"])
.all(setDataset(req => req.params[0]))
.getAsync(getDataset)
.optionsAsync(optionsDataset)
;


Expand All @@ -284,11 +292,13 @@ app.use(["/fetch/narratives/:authority", "/fetch/:authority"],
app.routeAsync("/fetch/narratives/:authority/*")
.all(setNarrative(req => req.params[0]))
.getAsync(getNarrative)
.optionsAsync(optionsNarrative)
;

app.routeAsync("/fetch/:authority/*")
.all(setDataset(req => req.params[0]))
.getAsync(getDataset)
.optionsAsync(optionsDataset)
;


Expand Down Expand Up @@ -327,14 +337,14 @@ app.routeAsync("/groups/:groupName/settings/logo")
.getAsync(getGroupLogo)
.putAsync(putGroupLogo)
.deleteAsync(deleteGroupLogo)
.optionsAsync(optionsGroupSettings)
.optionsAsync(optionsGroup)
;

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

app.route("/groups/:groupName/settings/*")
Expand All @@ -349,13 +359,15 @@ app.routeAsync("/groups/:groupName/narratives/*")
.getAsync(getNarrative)
.putAsync(putNarrative)
.deleteAsync(deleteNarrative)
.optionsAsync(optionsNarrative)
;

app.routeAsync("/groups/:groupName/*")
.all(setDataset(req => req.params[0]))
.getAsync(getDataset)
.putAsync(putDataset)
.deleteAsync(deleteDataset)
.optionsAsync(optionsDataset)
;


Expand Down
15 changes: 3 additions & 12 deletions src/endpoints/groups.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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";
import * as options from "./options.js";


const setGroup = (nameExtractor) => (req, res, next) => {
Expand All @@ -17,18 +18,8 @@ const setGroup = (nameExtractor) => (req, res, next) => {
/* Group customizations
*/

const optionsGroupSettings = (req, res) => {
authz.assertAuthorized(req.user, authz.actions.Read, req.context.group);

const allowedMethods = ["OPTIONS", "GET", "HEAD"];

if (authz.authorized(req.user, authz.actions.Write, req.context.group)) {
allowedMethods.push("PUT", "DELETE");
}
const optionsGroup = options.forAuthzObject(req => req.context.group);

res.set("Allow", allowedMethods);
return res.status(204).end();
};

/* Group logo
*/
Expand Down Expand Up @@ -163,7 +154,7 @@ async function receiveGroupLogo(req, res) {

export {
setGroup,
optionsGroupSettings,
optionsGroup,
getGroupLogo,
putGroupLogo,
deleteGroupLogo,
Expand Down
2 changes: 2 additions & 0 deletions src/endpoints/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as charon from './charon/index.js';
import * as cli from './cli.js';
import * as groups from "./groups.js";
import * as options from './options.js';
import * as sources from './sources.js';
import * as static_ from './static.js';
import * as users from './users.js';
Expand All @@ -9,6 +10,7 @@ export {
charon,
cli,
groups,
options,
sources,
static_ as static,
users,
Expand Down
37 changes: 37 additions & 0 deletions src/endpoints/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as authz from "../authz/index.js";


/**
* Generate an Express endpoint that responds to an OPTIONS request with an
* Allow header based on the current user's authorized actions on an object
* determined by the request.
*
* The Allow header will include the OPTIONS, GET, and HEAD methods if the
* current user is granted {@link module:../authz/actions.Read} and PUT and
* DELETE if {@link module:../authz/actions.Write}.
*
* @param {authzObjectExtractor} authzObjectExtractor - Function to provide the object of authorization checks from the request
* @returns {expressEndpoint}
* @throws {module:../exceptions.AuthzDenied} if {@link module:../authz/actions.Read} is not authorized
*/
export const forAuthzObject = (authzObjectExtractor) => (req, res) => {
const authzObject = authzObjectExtractor(req);

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

const allowedMethods = ["OPTIONS", "GET", "HEAD"];

if (authz.authorized(req.user, authz.actions.Write, authzObject)) {
allowedMethods.push("PUT", "DELETE");
}

res.set("Allow", allowedMethods);
return res.status(204).end();
};


/**
* @callback authzObjectExtractor
* @param {express.request} req
* @returns {object} An object supported by the authz system.
*/
11 changes: 9 additions & 2 deletions src/endpoints/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NotFound } from '../httpErrors.js';

import * as authz from '../authz/index.js';
import { contentTypesProvided, contentTypesConsumed } from '../negotiate.js';
import * as options from './options.js';
import { sendAuspiceEntrypoint } from './static.js';
import { deleteByUrls, proxyFromUpstream, proxyToUpstream } from "../upstream.js";

Expand All @@ -17,8 +18,6 @@ import { deleteByUrls, proxyFromUpstream, proxyToUpstream } from "../upstream.js
const setSource = (sourceExtractor) => (req, res, next) => {
const source = sourceExtractor(req);

res.vary("Accept");

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

req.context.source = source;
Expand Down Expand Up @@ -179,6 +178,12 @@ const deleteDataset = deleteResource(req => req.context.dataset);
const deleteNarrative = deleteResource(req => req.context.narrative);


/* OPTIONS
*/
const optionsDataset = options.forAuthzObject(req => req.context.dataset);
const optionsNarrative = options.forAuthzObject(req => req.context.narrative);


/* Narratives
*/

Expand Down Expand Up @@ -397,12 +402,14 @@ export {
getDataset,
putDataset,
deleteDataset,
optionsDataset,

setNarrative,
ifNarrativeExists,
getNarrative,
putNarrative,
deleteNarrative,
optionsNarrative,

sendDatasetSubresource,
sendNarrativeSubresource,
Expand Down

0 comments on commit 64beb5a

Please sign in to comment.