11import httpContext from 'express-http-context' ;
2- import SC2 from 'sparql-client-2' ;
32import env from 'env-var' ;
4-
5- const { SparqlClient, SPARQL } = SC2 ;
3+ import SPARQL from './sparql-tag.js' ;
64
75const LOG_SPARQL_QUERIES = process . env . LOG_SPARQL_QUERIES != undefined ? env . get ( 'LOG_SPARQL_QUERIES' ) . asBool ( ) : env . get ( 'LOG_SPARQL_ALL' ) . asBool ( ) ;
86const LOG_SPARQL_UPDATES = process . env . LOG_SPARQL_UPDATES != undefined ? env . get ( 'LOG_SPARQL_UPDATES' ) . asBool ( ) : env . get ( 'LOG_SPARQL_ALL' ) . asBool ( ) ;
97const DEBUG_AUTH_HEADERS = env . get ( 'DEBUG_AUTH_HEADERS' ) . asBool ( ) ;
8+ const MU_SPARQL_ENDPOINT = process . env . MU_SPARQL_ENDPOINT || 'http://database:8890/sparql' ;
109
1110//==-- logic --==//
1211
13- // builds a new sparqlClient
14- function newSparqlClient ( userOptions ) {
15- let options = { requestDefaults : { headers : { } } } ;
16-
17- if ( userOptions . sudo === true ) {
18- if ( env . get ( "ALLOW_MU_AUTH_SUDO" ) . asBool ( ) ) {
19- options . requestDefaults . headers [ 'mu-auth-sudo' ] = "true" ;
20- } else {
21- throw "Error, sudo request but service lacks ALLOW_MU_AUTH_SUDO header" ;
22- }
23- }
24-
25- if ( userOptions . scope ) {
26- options . requestDefaults . headers [ 'mu-auth-scope' ] = userOptions . scope ;
27- } else if ( process . env . DEFAULT_MU_AUTH_SCOPE ) {
28- options . requestDefaults . headers [ 'mu-auth-scope' ] = process . env . DEFAULT_MU_AUTH_SCOPE ;
29- }
30-
31- if ( httpContext . get ( 'request' ) ) {
32- options . requestDefaults . headers [ 'mu-session-id' ] = httpContext . get ( 'request' ) . get ( 'mu-session-id' ) ;
33- options . requestDefaults . headers [ 'mu-call-id' ] = httpContext . get ( 'request' ) . get ( 'mu-call-id' ) ;
34- options . requestDefaults . headers [ 'mu-auth-allowed-groups' ] = httpContext . get ( 'request' ) . get ( 'mu-auth-allowed-groups' ) ; // groups of incoming request
35- }
36-
37- if ( httpContext . get ( 'response' ) ) {
38- const allowedGroups = httpContext . get ( 'response' ) . get ( 'mu-auth-allowed-groups' ) ; // groups returned by a previous SPARQL query
39- if ( allowedGroups )
40- options . requestDefaults . headers [ 'mu-auth-allowed-groups' ] = allowedGroups ;
41- }
42-
43- if ( DEBUG_AUTH_HEADERS ) {
44- console . log ( `Headers set on SPARQL client: ${ JSON . stringify ( options ) } ` ) ;
45- }
46-
47- return new SparqlClient ( process . env . MU_SPARQL_ENDPOINT , options ) ;
48- }
49-
5012/**
51- * @typedef {Object } QueryOptions
52- * @property {boolean? } sudo Execute the query as sudo
53- * @property {string? } scope URI of the scope with whith the query is executed. Use the environment variable `DEFAULT_MU_AUTH_SCOPE` if possible.
54- */
55-
56- /**
57- * Execute a sparql QUERY. Intended for use with QUERY and ASK.
13+ * Execute a sparql QUERY. Intended for use with SELECT and ASK.
5814 *
5915 * See environment variables for logging: `LOG_SPARQL_ALL`, `LOG_SPARQL_QUERIES`, `DEBUG_AUTH_HEADERS`
6016 *
6117 * @param { string } queryString SPARQL query as a string.
6218 * @param { QueryOptions? } options Operational changes to the SPARQL query.
6319 * @return { Promise<object?> } The response is returned as a parsed JSON object, or null if the response could not be parsed as JSON.
6420 */
65- function query ( queryString , options ) {
21+ function query ( queryString , options = { } ) {
6622 if ( LOG_SPARQL_QUERIES ) {
6723 console . log ( queryString ) ;
6824 }
6925 return executeQuery ( queryString , options ) ;
70- } ;
26+ }
7127
7228/**
73- * Execute a sparql QUERY .
29+ * Execute a sparql UPDATE .
7430 * Intended for use with `DELETE {} INSERT {} WHERE {}`, `INSERT DATA` and `DELETE DATA`.
7531 *
7632 * See environment variables for logging: `LOG_SPARQL_ALL`, `LOG_SPARQL_UPDATES`, `DEBUG_AUTH_HEADERS`
@@ -79,58 +35,150 @@ function query( queryString, options ) {
7935 * @param { QueryOptions? } options Operational changes to the SPARQL query.
8036 * @return { Promise<object?> } The response is returned as a parsed JSON object, or null if the response could not be parsed as JSON.
8137 */
82- function update ( queryString , options ) {
38+ function update ( queryString , options = { } ) {
8339 if ( LOG_SPARQL_UPDATES ) {
8440 console . log ( queryString ) ;
8541 }
8642 return executeQuery ( queryString , options ) ;
87- } ;
43+ }
44+
45+ /**
46+ * Build the default headers for a SPARQL request from the current HTTP
47+ * context, forwarding mu-auth headers so mu-authorization can apply the
48+ * correct access rules.
49+ */
50+ function defaultHeaders ( ) {
51+ const headers = new Headers ( ) ;
52+ headers . set ( 'content-type' , 'application/x-www-form-urlencoded' ) ;
53+ headers . set ( 'Accept' , 'application/sparql-results+json' ) ;
54+
55+ const req = httpContext . get ( 'request' ) ;
56+ if ( req ) {
57+ const muSessionId = req . get ( 'mu-session-id' ) ;
58+ if ( muSessionId ) headers . set ( 'mu-session-id' , muSessionId ) ;
59+
60+ const muCallId = req . get ( 'mu-call-id' ) ;
61+ if ( muCallId ) headers . set ( 'mu-call-id' , muCallId ) ;
62+
63+ // Forward allowed-groups from the incoming request so mu-authorization
64+ // does not have to recompute them on every SPARQL call.
65+ const allowedGroups = req . get ( 'mu-auth-allowed-groups' ) ;
66+ if ( allowedGroups ) headers . set ( 'mu-auth-allowed-groups' , allowedGroups ) ;
67+ }
68+
69+ const res = httpContext . get ( 'response' ) ;
70+ if ( res ) {
71+ // If a previous SPARQL query within this request already resolved the
72+ // allowed groups, forward them to avoid redundant lookups.
73+ const allowedGroups = res . get ( 'mu-auth-allowed-groups' ) ;
74+ if ( allowedGroups ) headers . set ( 'mu-auth-allowed-groups' , allowedGroups ) ;
75+ }
76+
77+ return headers ;
78+ }
8879
89- function executeQuery ( queryString , options ) {
90- return newSparqlClient ( options || { } ) . query ( queryString ) . executeRaw ( ) . then ( response => {
91- const temp = httpContext ;
92-
93- if ( httpContext . get ( 'response' ) && ! httpContext . get ( 'response' ) . headersSent ) {
94- // set mu-auth-allowed-groups on outgoing response
95- const allowedGroups = response . headers [ 'mu-auth-allowed-groups' ] ;
96- if ( allowedGroups ) {
97- httpContext . get ( 'response' ) . setHeader ( 'mu-auth-allowed-groups' , allowedGroups ) ;
98- if ( DEBUG_AUTH_HEADERS ) {
99- console . log ( `Update mu-auth-allowed-groups to ${ allowedGroups } ` ) ;
100- }
101- } else {
102- httpContext . get ( 'response' ) . removeHeader ( 'mu-auth-allowed-groups' ) ;
103- if ( DEBUG_AUTH_HEADERS ) {
104- console . log ( 'Remove mu-auth-allowed-groups' ) ;
105- }
106- }
107-
108- // set mu-auth-used-groups on outgoing response
109- const usedGroups = response . headers [ 'mu-auth-used-groups' ] ;
110- if ( usedGroups ) {
111- httpContext . get ( 'response' ) . setHeader ( 'mu-auth-used-groups' , usedGroups ) ;
112- if ( DEBUG_AUTH_HEADERS ) {
113- console . log ( `Update mu-auth-used-groups to ${ usedGroups } ` ) ;
114- }
115- } else {
116- httpContext . get ( 'response' ) . removeHeader ( 'mu-auth-used-groups' ) ;
117- if ( DEBUG_AUTH_HEADERS ) {
118- console . log ( 'Remove mu-auth-used-groups' ) ;
119- }
120- }
80+ /**
81+ * @typedef {Object } QueryOptions
82+ * @property {boolean? } sudo Execute the query with mu-auth-sudo privileges.
83+ * @property {string? } scope URI of the scope to use. Falls back to the DEFAULT_MU_AUTH_SCOPE environment variable.
84+ * @property {object? } extraHeaders Additional headers to include in the request.
85+ */
86+
87+ /**
88+ * Send a SPARQL query to the configured endpoint and return the parsed JSON
89+ * response.
90+ *
91+ * @param { string } queryString SPARQL query as a string.
92+ * @param { QueryOptions? } options Operational changes to the SPARQL query.
93+ * @return { Promise<object?> } The response is returned as a parsed JSON object, or null if the response could not be parsed as JSON.
94+ */
95+ async function executeQuery ( queryString , options = { } ) {
96+ const headers = defaultHeaders ( ) ;
97+
98+ const extraHeaders = options . extraHeaders ?? { } ;
99+ for ( const key of Object . keys ( extraHeaders ) ) {
100+ headers . append ( key , extraHeaders [ key ] ) ;
101+ }
102+
103+ if ( options . sudo === true ) {
104+ if ( env . get ( 'ALLOW_MU_AUTH_SUDO' ) . asBool ( ) ) {
105+ headers . set ( 'mu-auth-sudo' , 'true' ) ;
106+ } else {
107+ throw new Error ( 'sudo query requested but ALLOW_MU_AUTH_SUDO is not set' ) ;
121108 }
109+ }
110+
111+ if ( options . scope ) {
112+ headers . set ( 'mu-auth-scope' , options . scope ) ;
113+ } else if ( process . env . DEFAULT_MU_AUTH_SCOPE ) {
114+ headers . set ( 'mu-auth-scope' , process . env . DEFAULT_MU_AUTH_SCOPE ) ;
115+ }
116+
117+ if ( DEBUG_AUTH_HEADERS ) {
118+ const muHeaders = Array . from ( headers . entries ( ) )
119+ . filter ( ( [ key ] ) => key . startsWith ( 'mu-' ) )
120+ . map ( ( [ key , value ] ) => `${ key } : ${ value } ` )
121+ . join ( ', ' ) ;
122+ console . log ( `SPARQL request mu-headers: ${ muHeaders } ` ) ;
123+ }
124+
125+ const formData = new URLSearchParams ( ) ;
126+ formData . set ( 'query' , queryString ) ;
122127
123- function maybeParseJSON ( body ) {
124- // Catch invalid JSON
125- try {
126- return JSON . parse ( body ) ;
127- } catch ( ex ) {
128- return null ;
129- }
128+ try {
129+ const response = await fetch ( MU_SPARQL_ENDPOINT , {
130+ method : 'POST' ,
131+ body : formData . toString ( ) ,
132+ headers,
133+ } ) ;
134+
135+ updateResponseHeaders ( response ) ;
136+
137+ if ( ! response . ok ) {
138+ throw new Error ( `SPARQL endpoint returned HTTP ${ response . status } ${ response . statusText } ` ) ;
130139 }
131140
132- return maybeParseJSON ( response . body ) ;
133- } ) ;
141+ return await maybeJSON ( response ) ;
142+ } catch ( ex ) {
143+ console . log ( `Failed Query:
144+ ${ queryString } `) ;
145+ throw ex ;
146+ }
147+ }
148+
149+ /**
150+ * Copy mu-auth group headers from the SPARQL response back onto the outgoing
151+ * HTTP response so the client receives up-to-date group information.
152+ */
153+ function updateResponseHeaders ( response ) {
154+ const res = httpContext . get ( 'response' ) ;
155+ if ( ! res || res . headersSent ) return ;
156+
157+ const allowedGroups = response . headers . get ( 'mu-auth-allowed-groups' ) ;
158+ if ( allowedGroups ) {
159+ res . setHeader ( 'mu-auth-allowed-groups' , allowedGroups ) ;
160+ if ( DEBUG_AUTH_HEADERS ) console . log ( `Forwarded mu-auth-allowed-groups: ${ allowedGroups } ` ) ;
161+ } else {
162+ res . removeHeader ( 'mu-auth-allowed-groups' ) ;
163+ if ( DEBUG_AUTH_HEADERS ) console . log ( 'Removed mu-auth-allowed-groups from response' ) ;
164+ }
165+
166+ const usedGroups = response . headers . get ( 'mu-auth-used-groups' ) ;
167+ if ( usedGroups ) {
168+ res . setHeader ( 'mu-auth-used-groups' , usedGroups ) ;
169+ if ( DEBUG_AUTH_HEADERS ) console . log ( `Forwarded mu-auth-used-groups: ${ usedGroups } ` ) ;
170+ } else {
171+ res . removeHeader ( 'mu-auth-used-groups' ) ;
172+ if ( DEBUG_AUTH_HEADERS ) console . log ( 'Removed mu-auth-used-groups from response' ) ;
173+ }
174+ }
175+
176+ async function maybeJSON ( response ) {
177+ try {
178+ return await response . json ( ) ;
179+ } catch ( _ ) {
180+ return null ;
181+ }
134182}
135183
136184/**
@@ -198,9 +246,10 @@ function sparqlEscapeDate( value ){
198246} ;
199247
200248/**
201- * Escapes a date string or date object into an xsd:dateTime for use in a SPARQL.
249+ * Escape date string or date object into an xsd:dateTime for use in a SPARQL string .
202250 *
203- * @param { Date | string | number } value Date representation (understood by `new Date`) to convert.
251+ * @param { Date | string | number } value Date representation
252+ * (understood by `new Date`) to convert.
204253 * @return { string } Date representation for SPARQL query.
205254 */
206255function sparqlEscapeDateTime ( value ) {
@@ -217,14 +266,6 @@ function sparqlEscapeBool( value ){
217266 return value ? '"true"^^xsd:boolean' : '"false"^^xsd:boolean' ;
218267} ;
219268
220- /**
221- * Escapes a value based on the supplide type rather than the separately published functions. Prefer to use the
222- * functions.
223- *
224- * @param { "string"|"uri"|"bool"|"decimal"|"int"|"float"|"date"|"dateTime" } type The value to be escaped.
225- * @param {* } value The value to be escaped.
226- * @return { string } Boolean representation for SPARQL query.
227- */
228269function sparqlEscape ( value , type ) {
229270 switch ( type ) {
230271 case 'string' :
@@ -251,25 +292,23 @@ function sparqlEscape( value, type ){
251292
252293//==-- exports --==//
253294const exports = {
254- newSparqlClient : newSparqlClient ,
255- SPARQL : SPARQL ,
295+ SPARQL ,
256296 sparql : SPARQL ,
257- query : query ,
258- update : update ,
259- sparqlEscape : sparqlEscape ,
260- sparqlEscapeString : sparqlEscapeString ,
261- sparqlEscapeUri : sparqlEscapeUri ,
262- sparqlEscapeDecimal : sparqlEscapeDecimal ,
263- sparqlEscapeInt : sparqlEscapeInt ,
264- sparqlEscapeFloat : sparqlEscapeFloat ,
265- sparqlEscapeDate : sparqlEscapeDate ,
266- sparqlEscapeDateTime : sparqlEscapeDateTime ,
267- sparqlEscapeBool : sparqlEscapeBool
268- }
297+ query,
298+ update,
299+ sparqlEscape,
300+ sparqlEscapeString,
301+ sparqlEscapeUri,
302+ sparqlEscapeDecimal,
303+ sparqlEscapeInt,
304+ sparqlEscapeFloat,
305+ sparqlEscapeDate,
306+ sparqlEscapeDateTime,
307+ sparqlEscapeBool,
308+ } ;
269309export default exports ;
270310
271311export {
272- newSparqlClient ,
273312 SPARQL as SPARQL ,
274313 SPARQL as sparql ,
275314 query ,
@@ -282,5 +321,5 @@ export {
282321 sparqlEscapeFloat ,
283322 sparqlEscapeDate ,
284323 sparqlEscapeDateTime ,
285- sparqlEscapeBool
324+ sparqlEscapeBool ,
286325} ;
0 commit comments