2024-03-04 15:25:27 +01:00
export default { enforce } ;
import defaultExport from './config.js' ;
function log ( c , s ) {
c . request . error ( s )
}
var _debug = true
function debug ( c , s ) {
if ( _debug === true ) {
log ( c , s )
}
}
function enforce ( r ) {
var context = {
request : r ,
config : defaultExport [ "config" ] ,
backend : ( defaultExport . backend ? defaultExport . backend : "/_backend" )
}
2024-03-06 13:36:32 +01:00
log ( context , "Inside NJS enforce for " + r . method + " @ " + r . headersIn . host + r . uri )
2024-03-04 15:25:27 +01:00
context = computeProtection ( context )
wkf . run ( wkf . build ( context ) , context )
}
// ######## WORKFLOW FUNCTIONS ###############
var wkf = {
build : ( context ) => {
2024-03-06 13:36:32 +01:00
//An example workflow for direct proxying to backend with no PIP and no Headers to export
2024-03-04 15:25:27 +01:00
var actions = [
"export_pep_credentials" ,
"parse_authentication" ,
"check_authentication" ,
"export_authn_token" ,
// "pip",
"pdp" ,
// "export_backend_headers",
"pass"
]
2024-03-06 13:36:32 +01:00
// An example workflow (with no PIP and no extra headers) that intercepts the response in order to complete an accounting record which is started at the receipt of the original request
2024-03-04 15:25:27 +01:00
/ * v a r a c t i o n s = [
"export_pep_credentials" ,
"parse_authentication" ,
"check_authentication" ,
"export_authn_token" ,
2024-03-06 13:36:32 +01:00
//"pip",
2024-03-04 15:25:27 +01:00
"pdp" ,
2024-03-06 13:36:32 +01:00
//"export_backend_headers",
2024-03-04 15:25:27 +01:00
"start_accounting" ,
"pass_and_wait" ,
"close_accounting" ,
"respond_to_client"
] * /
return actions
} ,
run : ( actions , context ) => {
context . request . error ( "Starting workflow with " + njs . dump ( actions ) )
var w = actions . reduce (
( acc , f ) => { return acc . then ( typeof ( f ) === "function" ? f : wkf [ f ] ) } ,
Promise . resolve ( ) . then ( ( ) => context )
)
w . catch ( e => { context . request . error ( njs . dump ( e ) ) ; context . request . return ( 401 ) } )
} ,
export _pep _credentials : exportPepCredentials ,
export _authn _token : exportAuthToken ,
export _backend _headers : exportBackendHeaders ,
parse _authentication : parseAuthentication ,
check _authentication : checkAuthentication ,
verify _token : verifyToken ,
request _token : requestToken ,
pip : pipExecutor ,
pdp : pdpExecutor ,
pass : pass ,
pass _and _wait : pass _and _wait ,
respond _to _client : respondToClient ,
start _accounting : buildAccountingRecord ,
close _accounting : closeAccountingRecord ,
send _accounting : sendAccountingRecord
}
function getTokenField ( context , f ) {
return context . authn . verified _token [ f ]
}
function exportVariable ( context , name , value ) {
context . request . variables [ name ] = value
return context
}
function exportBackendHeaders ( context ) {
return context
}
function exportPepCredentials ( context ) {
if ( process . env [ "pep_credentials" ] || process . env [ "PEP_CREDENTIALS" ] ) {
return exportVariable ( context , "pep_credentials" , "Basic " + process . env [ "PEP_CREDENTIALS" ] )
} else if ( context . config [ "pep_credentials" ] ) {
return exportVariable ( context , "pep_credentials" , "Basic " + context . config [ "pep_credentials" ] )
} else {
throw new Error ( "Need PEP credentials" )
}
}
function exportAuthToken ( context ) {
return exportVariable ( context , "auth_token" , context . authn . token )
}
function checkAuthentication ( context ) {
return context . authn . type === "bearer" ? wkf . verify _token ( context ) : wkf . request _token ( context )
}
function parseAuthentication ( context ) {
context . request . log ( "Inside parseAuthentication" )
var incomingauth = context . request . headersIn [ "Authorization" ]
if ( ! incomingauth ) throw new Error ( "Authentication required" ) ;
var arr = incomingauth . trim ( ) . replace ( /\s\s+/g , " " ) . split ( " " )
if ( arr . length != 2 ) throw new Error ( "Unknown authentication scheme" ) ;
var type = arr [ 0 ] . toLowerCase ( )
2024-03-06 13:36:32 +01:00
if ( type === "basic" && context . authz . host && ( context . authz . host [ "allow-basic-auth" ] || context . authz . host [ "allow_basic_auth" ] ) ) {
2024-03-04 15:25:27 +01:00
var unamepass = Buffer . from ( arr [ 1 ] , 'base64' ) . toString ( ) . split ( ":" )
if ( unamepass . length != 2 ) return null ;
context . authn = { type : type , raw : arr [ 1 ] , user : unamepass [ 0 ] , password : unamepass [ 1 ] }
return context
} else if ( type === "bearer" ) {
context . authn = { type : type , raw : arr [ 1 ] , token : arr [ 1 ] }
return context
}
throw new Error ( "Unknown authentication scheme" ) ;
}
function verifyToken ( context ) {
log ( context , "Inside verifyToken" )
debug ( context , "Token is " + context . authn . token )
var options = {
"body" : "token=" + context . authn . token + "&token_type_hint=access_token"
}
return context . request . subrequest ( "/jwt_verify_request" , options )
. then ( reply => {
if ( reply . status === 200 ) {
var response = JSON . parse ( reply . responseText ) ;
if ( response . active === true ) {
return response
} else {
throw new Error ( "Unauthorized: " + reply . responseText )
}
} else {
throw new Error ( "Unauthorized: " + reply . responseText )
}
} ) . then ( verified _token => {
context . authn . verified _token =
JSON . parse ( Buffer . from ( context . authn . token . split ( '.' ) [ 1 ] , 'base64url' ) . toString ( ) )
return context
} )
}
function requestToken ( context ) {
log ( context , "Inside requestToken" )
var options = {
"body" : "grant_type=client_credentials&client_id=" + context . authn . user + "&client_secret=" + context . authn . password
}
return context . request . subrequest ( "/jwt_request" , options )
. then ( reply => {
if ( reply . status === 200 ) {
var response = JSON . parse ( reply . responseText ) ;
context . authn . token = response . access _token
context . authn . verified _token =
JSON . parse ( Buffer . from ( context . authn . token . split ( '.' ) [ 1 ] , 'base64url' ) . toString ( ) )
return context
} else if ( reply . status === 400 || reply . status === 401 ) {
var options = {
"body" : "grant_type=password&username=" + context . authn . user + "&password=" + context . authn . password
}
return context . request . subrequest ( "/jwt_request" , options )
. then ( reply => {
if ( reply . status === 200 ) {
var response = JSON . parse ( reply . responseText ) ;
context . authn . token = response . access _token
context . authn . verified _token =
JSON . parse ( Buffer . from ( context . authn . token . split ( '.' ) [ 1 ] , 'base64url' ) . toString ( ) )
return context
} else {
throw new Error ( "Unauthorized " + reply . status )
}
} )
} else {
throw new Error ( "Unauthorized " + reply . status )
}
} )
}
function pipExecutor ( context ) {
log ( context , "Inside extra claims PIP" )
context . authz . pip . forEach ( extra => {
//call extra claim pip function
try {
var operator = extra . operator
var result = wkf [ operator ] ( context , extra . args )
//ensure array and add to extra_claims
if ( ! ( result instanceof Array ) ) result = [ result ]
if ( ! context . extra _claims ) context . extra _claims = { } ;
context . extra _claims [ extra . claim ] = result
} catch ( error ) {
log ( context , "Skipping invalid extra claim " + njs . dump ( error ) )
}
} )
log ( context , "Extra claims are " + njs . dump ( context . extra _claims ) )
return context
}
function pdpExecutor ( context ) {
log ( context , "Inside PDP" )
return context . authz . pdp ( context )
}
function umaCall ( context ) {
log ( context , "Inside UMA call" )
var options = { "body" : computePermissionRequestBody ( context ) } ;
return context . request . subrequest ( "/permission_request" , options )
. then ( reply => {
if ( reply . status === 200 ) {
debug ( context , "UMA call reply is " + reply . status )
return context
} else {
throw new Error ( "Response for authorization request is not ok " + reply . status + " " + njs . dump ( reply . responseText ) )
}
} )
}
// Call backend and return reply to client directly
async function pass ( context ) {
2024-03-06 13:36:32 +01:00
log ( context , "Inside pass: " ) ;
2024-03-04 15:25:27 +01:00
const r = context . request
2024-03-06 13:36:32 +01:00
const reply = await r . subrequest ( context . backend , { method : r . method , args : r . variables . args , headers : r . headersIn } )
2024-03-04 15:25:27 +01:00
debug ( context , "[BACKEND] response status: " + reply . status )
context . backendresponse = reply
return respondToClient ( context )
}
// Pass to backend but instead of returning to client wait for the reply. Thi intercepts the possibility of performing extra action like closing and sending accounting record.
async function pass _and _wait ( context ) {
log ( context , "Inside pass and wait" ) ;
const r = context . request
2024-03-06 13:36:32 +01:00
const reply = await r . subrequest ( context . backend , { method : r . method , args : r . variables . args , headers : r . headersIn } )
2024-03-04 15:25:27 +01:00
debug ( context , "[BACKEND] response status: " + reply . status )
context . backendresponse = reply
return context
}
// Definetely return backend response
function respondToClient ( context ) {
log ( context , "Inside respond to client" + njs . dump ( context . backendresponse ) ) ;
for ( let k in context . backendresponse . headersOut ) {
context . request . headersOut [ k ] = context . backendresponse . headersOut [ k ]
}
context . request . return ( context . backendresponse . status , context . backendresponse . responseText )
return context
}
// ######## AUTHORIZATION PART ###############
function computePermissionRequestBody ( context ) {
if ( ! context . authz . host || ! context . authz . path ) {
throw new Error ( "Enforcemnt mode is always enforcing. Host or path not found..." )
}
var audience = computeAudience ( context )
var grant = "grant_type=urn:ietf:params:oauth:grant-type:uma-ticket"
var mode = "response_mode=decision"
var permissions = computePermissions ( context )
var extra = ""
if ( context . extra _claims ) {
extra =
"claim_token_format=urn:ietf:params:oauth:token-type:jwt&claim_token=" +
JSON . stringify ( context . extra _claims ) . toString ( "base64url" )
}
var body = audience + "&" + grant + "&" + permissions + "&" + mode + "&" + extra
context . request . error ( "Computed permission request body is " + body )
return body
}
function computeAudience ( context ) {
var aud = context . request . headersIn . host
if ( context . authz . host ) {
aud = context . authz . host . audience || context . authz . host . host
}
return "audience=" + aud
}
function computePermissions ( context ) {
var resource = context . request . uri
if ( context . authz . path ) {
resource = context . authz . path . name || context . authz . path . path
}
var scopes = [ ]
if ( context . authz . method && context . authz . method . scopes ) {
scopes = context . authz . method . scopes
}
if ( scopes . length > 0 ) {
return scopes . map ( s => "permission=" + resource + "#" + s ) . join ( "&" )
}
return "permission=" + resource
}
function getPath ( hostconfig , incomingpath , incomingmethod ) {
var paths = hostconfig . paths || [ ]
var matchingpaths = paths
. filter ( p => { return incomingpath . match ( p . path ) != null } )
. reduce ( ( acc , p ) => {
if ( ! p . methods || p . methods . length === 0 ) acc . weak . push ( { path : p } ) ;
else {
var matchingmethods = p . methods . filter ( m => m . method . toUpperCase ( ) === incomingmethod )
if ( matchingmethods . length > 0 ) acc . strong . push ( { method : matchingmethods [ 0 ] , path : p } ) ;
}
return acc ;
} , { strong : [ ] , weak : [ ] } )
return matchingpaths . strong . concat ( matchingpaths . weak ) [ 0 ]
}
function getHost ( config , host ) {
var matching = config . hosts . filter ( h => {
//compare for both string and array of strings
return ( ( h . host . filter && h . host . indexOf ( host ) !== - 1 ) || h . host === host )
} )
return matching . length > 0 ? matching [ 0 ] : null
}
function computeProtection ( context ) {
debug ( context , "Getting by host " + context . request . headersIn . host )
context . authz = { }
context . authz . host = getHost ( context . config , context . request . headersIn . host )
if ( context . authz . host !== null ) {
log ( context , "Host found:" + context . authz . host )
context . authz . pip = context . authz . host . pip ? context . authz . host . pip : [ ] ;
context . authz . pdp = context . authz . host . pdp ? context . authz . host . pdp : umaCall ;
var pathandmethod = getPath ( context . authz . host , context . request . uri , context . request . method ) ;
if ( pathandmethod ) {
context . authz . path = pathandmethod . path ;
context . authz . pip = context . authz . path . pip ? context . authz . pip . concat ( context . authz . path . pip ) : context . authz . pip ;
context . authz . pdp = context . authz . path . pdp ? context . authz . path . pdp : context . authz . pdp ;
context . authz . method = pathandmethod . method ;
if ( context . authz . method ) {
context . authz . pip = context . authz . method . pip ? context . authz . pip . concat ( context . authz . method . pip ) : context . authz . pip ;
context . authz . pdp = context . authz . method . pdp ? context . authz . method . pdp : context . authz . pdp ;
}
}
}
debug ( context , "Leaving protection computation: " )
return context
}
// ####### ACCOUNTING PART #####################
function buildAccountingRecord ( context ) {
log ( context , "Inside build accounting record" ) ;
const t = ( new Date ( ) ) . getTime ( )
context . record = {
"recordType" : "ServiceUsageRecord" ,
"operationCount" : 1 ,
"creationTime" : t ,
"callerHost" : context . request . remoteAddress ,
"serviceClass" : "Application" ,
"callerQualifier" : "TOKEN" ,
"consumerId" : getTokenField ( context , "preferred_username" ) ,
"aggregated" : true ,
"serviceName" : "{{ SERVICENAME }}" ,
"duration" : 0 ,
"maxInvocationTime" : 0 ,
"scope" : "{{ SCOPES }}" ,
"host" : "{{ SERVICEHOST }}" ,
"startTime" : t ,
"id" : uuid ( ) ,
"calledMethod" : context . request . method + " " + context . request . uri ,
"endTime" : 0 ,
"minInvocationTime" : 0 ,
"operationResult" : null
}
debug ( context , "Record is " + JSON . stringify ( context . record ) )
return context
}
function closeAccountingRecord ( context , success ) {
log ( context , "Inside close accounting" ) ;
const t = ( new Date ( ) ) . getTime ( )
context . record . duration = t - context . record . startTime
context . record . endTime = t
context . record . minInvocationTime = context . record . duration
context . record . operationResult = success ? "SUCCESS" : "FAILED" ;
debug ( context , "Record is " + JSON . stringify ( context . record ) )
return context
}
function sendAccountingRecord ( context ) {
log ( context , "Inside send accounting" ) ;
2024-03-06 15:56:06 +01:00
context . request . subrequest ( "/accounting" , { detached : true , body : JSON . stringify ( [ context . record ] ) } )
2024-03-04 15:25:27 +01:00
return context
}
function uuid ( ) {
return 'xxxxxxxx-xxxx-4xxx-8xxx-xxxxxxxxxxxx' . replace ( /[x]/g , function ( c ) {
const r = Math . random ( ) * 16 | 0 , v = c == 'x' ? r : ( r & 0x3 | 0x8 ) ;
return v . toString ( 16 ) ;
} ) ;
}