Skip to content
Draft
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
33 changes: 31 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,8 @@ mu.app.get('/', function( req, res ) {
```

The following helper functions are provided by the template
- `query(query, options) => Promise`: Function for sending queries to the triplestore. Options is an object which may include `sudo` and `scope` keys.
- `update(query, options) => Promise`: Function for sending updates to the triplestore. Options is an object which may include `sudo` and `scope` keys.
- `query(query, options) => Promise`: Function for sending queries to the triplestore. Options is an object which may include `sudo`, `scope`, `allowedGroups` and `allowedGroupsHeader`.
- `update(query, options) => Promise`: Function for sending updates to the triplestore. Options is an object which may include `sudo`, `scope`, `allowedGroups` and `allowedGroupsHeader`.
- `uuid() => string`: Generates a random UUID (e.g. to construct new resource URIs)

The following SPARQL escape helpers are provided to construct safe SPARQL query strings
Expand All @@ -240,6 +240,34 @@ The following SPARQL escape helpers are provided to construct safe SPARQL query
- `sparqlEscapeDateTime(value) => string`
- `sparqlEscapeBool(value) => string`: The given value is evaluated to a boolean value in javascript. E.g. the string value `'0'` evaluates to `false` in javascript.
- `sparqlEscape(value, type) => string`: Function to escape a value in SPARQL according to the given type. Type must be one of `'string'`, `'uri'`, `'int'`, `'float'`, `'date'`, `'dateTime'`, `'bool'`.

#### Contextual SPARQL queries
You can use a context if multiple queries need the same options (sudo, headers, scope). You set the options once and they are used for all the following calls to query/update that happen inside the context You can always override a specific setting via the options parameter of query/update. Pass a `name` to have multiple contexts.
You can always override via the options parameter.

```javascript
import { CONTEXTUAL_QUERY, query, setAllowedGroups, setScope } from 'mu';

CONTEXTUAL_QUERY.run(() => {
setAllowedGroups([...]); // set allowed groups for all following queries in this context
setScope('http://example.com/scope'); // set scope for all following queries in this context
await query(`...`); // uses the set scope and allowed groups
await doInsert(); // queries inside this function use set scope and allowed groups
await query(`...`, {scope: 'http://example.com/scope2', allowedGroupsHeader: ''}) // override per query with options parameter

setScope('http://example.com/private-scope', 'privateScope'); // set scope for all following queries using the privateScope name
await query(`...`, { name: 'privateScope' });
});

function doInsert() {
await query(`...`);
}
```
- `CONTEXTUAL_QUERY`: namespace to start a context. Use `run` or `runAndReturn`.
- `setAllowedGroups(allowedGroups, name?)`, `setAllowedGroupsHeader(headerValue, name?)`: set an array of allowed groups or already serialized value for all following queries in the context (for the given name, optional).
- `setCurrentAllowedGroups(name?)`: Save the current `mu-auth-allowed-groups` header into the context. Only needed if you want to keep using the original allowed groups when leaving the httpContext (e.g. in a callback).
- `setScope(scope, name?)`: set the scope for all following queries.
- `getAllowedGroups`, `getAllowedGroupsHeader`, `getScope`

### Error handling
The template offers [an error handler](https://expressjs.com/en/guide/error-handling.html) to send error responses in a JSON:API compliant way. The handler can be imported from `'mu'` and need to be loaded at the end.
Expand Down Expand Up @@ -294,6 +322,7 @@ The verbosity of logging can be configured through following environment variabl
- `LOG_SPARQL_ALL`: Logging of all executed SPARQL queries, read as well as update (default `true`)
- `LOG_SPARQL_QUERIES`: Logging of executed SPARQL read queries (default: `undefined`). Overrules `LOG_SPARQL_ALL`.
- `LOG_SPARQL_UPDATES`: Logging of executed SPARQL update queries (default `undefined`). Overrules `LOG_SPARQL_ALL`.
- `LOG_SPARQL_RESULTS`: Logging of the raw SPARQL responses before they are parsed.
- `DEBUG_AUTH_HEADERS`: Debugging of [mu-authorization](https://github.com/mu-semtech/mu-authorization) access-control related headers (default `true`)

Following values are considered true: [`"true"`, `"TRUE"`, `"1"`].
Expand Down
125 changes: 108 additions & 17 deletions helpers/mu/sparql.js
Original file line number Diff line number Diff line change
@@ -1,46 +1,117 @@
import httpContext from 'express-http-context';
import SC2 from 'sparql-client-2';
import env from 'env-var';
import { createNamespace } from 'cls-hooked';

const { SparqlClient, SPARQL } = SC2;

const LOG_SPARQL_QUERIES = process.env.LOG_SPARQL_QUERIES != undefined ? env.get('LOG_SPARQL_QUERIES').asBool() : env.get('LOG_SPARQL_ALL').asBool();
const LOG_SPARQL_UPDATES = process.env.LOG_SPARQL_UPDATES != undefined ? env.get('LOG_SPARQL_UPDATES').asBool() : env.get('LOG_SPARQL_ALL').asBool();
const LOG_SPARQL_RESULTS = env.get('LOG_SPARQL_RESULTS').asBool();
const DEBUG_AUTH_HEADERS = env.get('DEBUG_AUTH_HEADERS').asBool();
const DEFAULT_MU_AUTH_SCOPE = process.env.DEFAULT_MU_AUTH_SCOPE;

const CONTEXTUAL_QUERY = createNamespace('contextualQuery');
const DEFAULT_NAMESPACE_KEY = 'contextualQueryDefaultNamespaceKey';
const ALLOWED_GROUPS_PREFIX = 'allowedGroups_';
const SCOPE_PREFIX = 'scope_';

function scopeName(name) { return `${SCOPE_PREFIX}${name}`; }
function allowedGroupsName(name) { return `${ALLOWED_GROUPS_PREFIX}${name}`; }

function setAllowedGroups(allowedGroups, name = DEFAULT_NAMESPACE_KEY) {
setAllowedGroupsHeader(JSON.stringify(allowedGroups), name);
}

function setCurrentAllowedGroups(name = DEFAULT_NAMESPACE_KEY) {
setAllowedGroupsHeader(httpContext.get('request')?.get('mu-auth-allowed-groups'), name);
}

function setAllowedGroupsHeader(allowedGroups, name = DEFAULT_NAMESPACE_KEY) {
CONTEXTUAL_QUERY.set(allowedGroupsName(name), allowedGroups);
}

function getAllowedGroupsHeader(name = DEFAULT_NAMESPACE_KEY) {
return CONTEXTUAL_QUERY.get(allowedGroupsName(name));
}

function getAllowedGroups(name = DEFAULT_NAMESPACE_KEY) {
return JSON.parse(getAllowedGroupsHeader(name));
}

function setScope(scope, name = DEFAULT_NAMESPACE_KEY) {
CONTEXTUAL_QUERY.set(scopeName(name), scope);
}

function getScope(name = DEFAULT_NAMESPACE_KEY) {
return CONTEXTUAL_QUERY.get(scopeName(name));
}

//==-- Usage (contextual query) --==//
// First import `CONTEXTUAL_QUERY`. Call `CONTEXTUAL_QUERY.run(() => contextedFunction())`
// with the function that should be run inside one context.
// use `CONTEXTUAL_QUERY.runAndReturn(() => contextedFunction())` to return the value returned by `contextedFunction()`.
// Inside this context, use `setAllowedGroupsHeader(allowedGroupsAsJsonString)` to set the allowed groups.
// use `setScopeHeader(scopeAsString)` to set the scope.
// Use the provided `update` and `query` functions that will query with these allowed groups.
//
// If you want to do queries with different allowed groups/scopes in the same context, use `setAllowedGroupsHeader(allowedGroupsString, name)`
// and `update(query, name)` and `query(query, name)`. With `name` a defined constant.
//
// Can also be used for enhanced regular query/update function.
// Can pass an options object to set the following values (overrides values set in context of namespace).
// - name: namespace key to use (instead of the default)
// - sudo: should it be a query sudo (send header for sudo query)
// - allowedGroups: allowed groups (as array) to set in the header
// - allowedGroupsHeader: allowed groups, already serialized to directly set as the header
// - scope: scope to set as header

//==-- logic --==//

// builds a new sparqlClient
function newSparqlClient(userOptions) {
function newSparqlClient(userOptions = {}) {
const {
name = DEFAULT_NAMESPACE_KEY,
sudo,
allowedGroups,
allowedGroupsHeader,
scope
} = userOptions;

let options = { requestDefaults: { headers: { } } };

if (userOptions.sudo === true) {
if (env.get("ALLOW_MU_AUTH_SUDO").asBool()) {
options.requestDefaults.headers['mu-auth-sudo'] = "true";
if (sudo === true) {
if (env.get('ALLOW_MU_AUTH_SUDO').asBool()) {
options.requestDefaults.headers['mu-auth-sudo'] = 'true';
} else {
throw "Error, sudo request but service lacks ALLOW_MU_AUTH_SUDO header";
throw 'Error, sudo request but service lacks ALLOW_MU_AUTH_SUDO header';
}
}

if (userOptions.scope) {
options.requestDefaults.headers['mu-auth-scope'] = userOptions.scope;
} else if (process.env.DEFAULT_MU_AUTH_SCOPE) {
options.requestDefaults.headers['mu-auth-scope'] = process.env.DEFAULT_MU_AUTH_SCOPE;
} else {
options.requestDefaults.headers['mu-auth-sudo'] = 'false';
}

if (httpContext.get('request')) {
options.requestDefaults.headers['mu-session-id'] = httpContext.get('request').get('mu-session-id');
options.requestDefaults.headers['mu-call-id'] = httpContext.get('request').get('mu-call-id');
options.requestDefaults.headers['mu-auth-allowed-groups'] = httpContext.get('request').get('mu-auth-allowed-groups'); // groups of incoming request
}

const resolvedAllowedGroupsHeader =
allowedGroupsHeader ||
(allowedGroups && JSON.stringify(allowedGroups)) ||
getAllowedGroupsHeader(name) ||
httpContext.get('request')?.get('mu-auth-allowed-groups');

if (resolvedAllowedGroupsHeader) {
options.requestDefaults.headers['mu-auth-allowed-groups'] = resolvedAllowedGroupsHeader;
}

if (httpContext.get('response')) {
const allowedGroups = httpContext.get('response').get('mu-auth-allowed-groups'); // groups returned by a previous SPARQL query
if (allowedGroups)
options.requestDefaults.headers['mu-auth-allowed-groups'] = allowedGroups;
const namespaceScope = getScope(name);
const scopeHeader = scope || namespaceScope || DEFAULT_MU_AUTH_SCOPE;
if (scopeHeader) {
options.requestDefaults.headers['mu-auth-scope'] = scopeHeader;
}

if (DEBUG_AUTH_HEADERS) {
console.log(`Namespace used for contextual query: ${name}`);
console.log(`Headers set on SPARQL client: ${JSON.stringify(options)}`);
}

Expand Down Expand Up @@ -129,6 +200,10 @@ function executeQuery( queryString, options ) {
}
}

if (LOG_SPARQL_RESULTS) {
console.log(`Query results: ${response.body}`);
}

return maybeParseJSON(response.body);
});
}
Expand Down Expand Up @@ -256,6 +331,14 @@ const exports = {
sparql: SPARQL,
query: query,
update: update,
CONTEXTUAL_QUERY: CONTEXTUAL_QUERY,
setAllowedGroups: setAllowedGroups,
setCurrentAllowedGroups: setCurrentAllowedGroups,
setAllowedGroupsHeader: setAllowedGroupsHeader,
getAllowedGroupsHeader: getAllowedGroupsHeader,
getAllowedGroups: getAllowedGroups,
setScope: setScope,
getScope: getScope,
sparqlEscape: sparqlEscape,
sparqlEscapeString: sparqlEscapeString,
sparqlEscapeUri: sparqlEscapeUri,
Expand All @@ -274,6 +357,14 @@ export {
SPARQL as sparql,
query,
update,
CONTEXTUAL_QUERY,
setAllowedGroups,
setCurrentAllowedGroups,
setAllowedGroupsHeader,
getAllowedGroupsHeader,
getAllowedGroups,
setScope,
getScope,
sparqlEscape,
sparqlEscapeString,
sparqlEscapeUri,
Expand Down