• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

restorecommerce / access-control-srv / 9790069742

04 Jul 2024 07:14AM UTC coverage: 74.751%. Remained the same
9790069742

push

github

Arun-KumarH
fix: revert removed event listeners and move reload to CRMD

498 of 670 branches covered (74.33%)

Branch coverage included in aggregate %.

66 of 111 new or added lines in 4 files covered. (59.46%)

322 existing lines in 4 files now uncovered.

2649 of 3540 relevant lines covered (74.83%)

54.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.83
/src/core/accessController.ts
1
import _ from 'lodash-es';
1✔
2
import {
1✔
3
  PolicySetWithCombinables, PolicyWithCombinables, AccessControlOperation,
1✔
4
  CombiningAlgorithm, AccessControlConfiguration, EffectEvaluation, ContextWithSubResolved
1✔
5
} from './interfaces.js';
1✔
6
import { Request, Response, Response_Decision, ReverseQuery } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/access_control.js';
1✔
7
import { Rule, RuleRQ, ContextQuery, Effect, Target } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/rule.js';
1✔
8
import { Policy, PolicyRQ } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/policy.js';
1✔
9
import { PolicySetRQ } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/policy_set.js';
1✔
10
import { Attribute } from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/attribute.js';
1✔
11
import {
1✔
12
  UserServiceClient
1✔
13
} from '@restorecommerce/rc-grpc-clients/dist/generated-server/io/restorecommerce/user.js';
1✔
14

1✔
15
import { ResourceAdapter } from './resource_adapters/adapter.js';
1✔
16
import { GraphQLAdapter } from './resource_adapters/gql.js';
1✔
17
import * as errors from './errors.js';
1✔
18
import { checkHierarchicalScope } from './hierarchicalScope.js';
1✔
19
import { Logger } from 'winston';
1✔
20
import { createClient, RedisClientType } from 'redis';
1✔
21
import { Topic } from '@restorecommerce/kafka-client';
1✔
22
import { verifyACLList } from './verifyACL.js';
1✔
23
import { conditionMatches } from './utils.js';
1✔
24

1✔
25
export class AccessController {
1✔
26
  policySets: Map<string, PolicySetWithCombinables>;
12✔
27
  combiningAlgorithms: Map<string, any>;
12✔
28
  urns: Map<string, string>;
12✔
29
  resourceAdapter: ResourceAdapter;
12✔
30
  redisClient: RedisClientType<any, any>;
12✔
31
  userTopic: Topic;
12✔
32
  waiting: any;
12✔
33
  cfg: any;
12✔
34
  userService: UserServiceClient;
12✔
35

12✔
36
  constructor(
12✔
37
    private logger: Logger,
12✔
38
    opts: AccessControlConfiguration,
12✔
39
    userTopic: Topic,
12✔
40
    cfg: any,
12✔
41
    userService: UserServiceClient
12✔
42
  ) {
12✔
43
    this.policySets = new Map<string, PolicySetWithCombinables>();
12✔
44
    this.combiningAlgorithms = new Map<string, any>();
12✔
45

12✔
46
    logger.info('Parsing combining algorithms from access control configuration...');
12✔
47
    //  parsing URNs and mapping them to functions
12✔
48
    const combiningAlgorithms: CombiningAlgorithm[] = opts?.combiningAlgorithms ?? [];
12!
49
    for (let ca of combiningAlgorithms) {
12✔
50
      const urn = ca.urn;
36✔
51
      const method = ca.method;
36✔
52

36✔
53
      if (this[method]) {
36✔
54
        this.combiningAlgorithms.set(urn, this[method]);
36✔
55
      } else {
36!
56
        logger.error('Unable to setup access controller: an invalid combining algorithm was found!');
×
57
        throw new errors.InvalidCombiningAlgorithm(urn);
×
UNCOV
58
      }
×
59
    }
36✔
60

12✔
61
    this.urns = new Map<string, string>();
12✔
62
    for (let urn in opts.urns || {}) {
12!
63
      this.urns.set(urn, opts.urns[urn]);
158✔
64
    }
158✔
65
    this.cfg = cfg;
12✔
66
    const redisConfig = this.cfg.get('redis');
12✔
67
    redisConfig.database = this.cfg.get('redis:db-indexes:db-subject');
12✔
68
    this.redisClient = createClient(redisConfig);
12✔
69
    this.redisClient.on('error', (err) => logger.error('Redis Client Error', { code: err.code, message: err.message, stack: err.stack }));
12✔
70
    this.redisClient.connect().then(data => logger.info('Redis client for subject cache connection successful')).catch(err => {
12✔
UNCOV
71
      logger.error('Error creating redis client instance', { code: err.code, message: err.message, stack: err.stack });
×
72
    });
12✔
73
    this.userTopic = userTopic;
12✔
74
    this.waiting = [];
12✔
75
    this.userService = userService;
12✔
76
  }
12✔
77

12✔
78
  clearPolicies(): void {
12✔
79
    this.policySets.clear();
×
80
  }
×
81

12✔
82
  /**
12✔
83
   * Method invoked for access control logic.
12✔
84
   *
12✔
85
   * @param request
12✔
86
   */
12✔
87
  async isAllowed(request: Request): Promise<Response> {
12✔
88

107✔
89
    this.logger.silly('Received an access request');
107✔
90
    if (!request.target) {
107!
UNCOV
91
      this.logger.silly('Access request had no target. Skipping request.');
×
UNCOV
92
      return {
×
UNCOV
93
        decision: Response_Decision.DENY,
×
UNCOV
94
        evaluation_cacheable: false, // typing for evaluation_cachecable expected so adding default false
×
UNCOV
95
        obligations: [],
×
UNCOV
96
        operation_status: {
×
UNCOV
97
          code: 400,
×
UNCOV
98
          message: 'Access request had no target. Skipping request'
×
UNCOV
99
        }
×
UNCOV
100
      };
×
UNCOV
101
    }
×
102

107✔
103
    let effect: EffectEvaluation;
107✔
104
    let obligations: Attribute[] = [];
107✔
105
    let context = (request as any).context as ContextWithSubResolved;
107✔
106
    if (!context) {
107✔
107
      (context as any) = {};
1✔
108
    }
1✔
109
    if (context?.subject?.token) {
107✔
110
      const subject = await this.userService.findByToken({ token: context.subject.token });
20✔
111
      if (subject?.payload) {
20✔
112
        context.subject.id = subject.payload.id;
20✔
113
        (context.subject as any).tokens = subject.payload.tokens;
20✔
114
        context.subject.role_associations = subject.payload.role_associations;
20✔
115
      }
20✔
116
    }
20✔
117

107✔
118
    // check if context subject_id contains HR scope if not make request 'createHierarchicalScopes'
107✔
119
    if (context?.subject?.token &&
107✔
120
      _.isEmpty(context.subject.hierarchical_scopes)) {
107✔
121
      context = await this.createHRScope(context);
20✔
122
    }
20✔
123

107✔
124
    for (let [, value] of this.policySets) {
107✔
125
      const policySet: PolicySetWithCombinables = value;
121✔
126
      let policyEffects: EffectEvaluation[] = [];
121✔
127

121✔
128
      // policyEffect needed to evalute if the properties should be PERMIT / DENY
121✔
129
      let policyEffect: Effect;
121✔
130
      if (
121✔
131
        !policySet.target
121✔
132
        || await this.targetMatches(policySet.target, request, 'isAllowed', obligations)
121✔
133
      ) {
121✔
134
        let exactMatch = false;
114✔
135
        for (let [, policyValue] of policySet.combinables) {
114✔
136
          const policy: Policy = policyValue;
169✔
137
          if (policy.effect) {
169✔
138
            policyEffect = policy.effect;
1✔
139
          }
1✔
140
          else if (policy.combining_algorithm) {
168✔
141
            const method = this.combiningAlgorithms.get(policy.combining_algorithm);
168✔
142
            if (method === 'permitOverrides') {
168!
UNCOV
143
              policyEffect = Effect.PERMIT;
×
144
            } else if (method === 'denyOverrides') {
168!
UNCOV
145
              policyEffect = Effect.DENY;
×
UNCOV
146
            }
×
147
          }
168✔
148

169✔
149
          if (
169✔
150
            policy.target
169✔
151
            && await this.targetMatches(policy.target, request, 'isAllowed', obligations, policyEffect)
169✔
152
          ) {
169✔
153
            exactMatch = true;
44✔
154
            break;
44✔
155
          }
44✔
156
        }
169✔
157

114✔
158
        // if there are multiple entities in the request.target.resources
114✔
159
        // and if exactMatch is true, then check again with the resourcesAttributeMatch providing one entity each time
114✔
160
        // to ensure there is an exact policy entity match for each of the requested entity
114✔
161
        const entityURN = this.urns.get('entity');
114✔
162
        if (exactMatch && request?.target?.resources?.filter(att => att?.id === entityURN)?.length > 1) {
114✔
163
          exactMatch = this.checkMultipleEntitiesMatch(value, request, obligations);
13✔
164
        }
13✔
165

114✔
166
        for (let [, policyValue] of policySet.combinables) {
114✔
167
          const policy: PolicyWithCombinables = policyValue;
221✔
168
          if (!policy) {
221!
UNCOV
169
            this.logger.debug('Policy Object not set');
×
UNCOV
170
            continue;
×
UNCOV
171
          }
×
172
          const ruleEffects: EffectEvaluation[] = [];
221✔
173
          if (
221✔
174
            !policy.target
221✔
175
            || (
221✔
176
              exactMatch
123✔
177
              && await this.targetMatches(policy.target, request, 'isAllowed', obligations, policyEffect)
123✔
178
            )
221✔
179
            // regex match
221✔
180
            || (
221✔
181
              !exactMatch
85✔
182
              && await this.targetMatches(policy.target, request, 'isAllowed', obligations, policyEffect, true)
85✔
183
            )
221✔
184
          ) {
221✔
185
            const rules: Map<string, Rule> = policy.combinables;
146✔
186
            this.logger.verbose(`Checking policy ${policy.name}`);
146✔
187
            let policySubjectMatch: boolean;
146✔
188
            // Subject set on Policy validate HR scope matching
146✔
189
            if (policy?.target?.subjects?.length > 0) {
146✔
190
              this.logger.verbose(`Checking Policy subject HR Scope match for ${policy.name}`);
7✔
191
              policySubjectMatch = await checkHierarchicalScope(policy.target, request, this.urns, this, this.logger);
7✔
192
            } else {
146✔
193
              policySubjectMatch = true;
139✔
194
            }
139✔
195
            // only apply a policy effect if there are no rules
146✔
196
            // combine rules otherwise
146✔
197
            if (rules.size == 0 && !!policy.effect) {
146✔
198
              policyEffects.push({ effect: policy.effect, evaluation_cacheable: policy.evaluation_cacheable });
1✔
199
            }
1✔
200
            else {
145✔
201
              let evaluationCacheableRule = true;
145✔
202
              for (let [, rule] of policy.combinables) {
145✔
203
                if (!rule) {
404!
UNCOV
204
                  this.logger.debug('Rule Object not set');
×
UNCOV
205
                  continue;
×
UNCOV
206
                }
×
207
                let evaluation_cacheable = rule.evaluation_cacheable;
404✔
208
                if (!evaluation_cacheable) {
404✔
209
                  evaluationCacheableRule = false;
404✔
210
                }
404✔
211
                // if rule has not target it should be always applied inside the policy scope
404✔
212
                this.logger.verbose(`Checking rule target and request target for ${rule.name}`);
404✔
213
                let matches = !rule.target || await this.targetMatches(rule.target, request, 'isAllowed', obligations, rule.effect);
404✔
214

326✔
215
                // check for regex if there is no direct match
326✔
216
                if (!matches) {
404✔
217
                  matches = await this.targetMatches(rule.target, request, 'isAllowed', obligations, rule.effect, true);
215✔
218
                }
215✔
219

404✔
220
                if (matches) {
404✔
221
                  this.logger.verbose(`Checking rule HR Scope for ${rule.name}`);
189✔
222
                  if (matches && rule.target) {
189✔
223
                    matches = await checkHierarchicalScope(rule.target, request, this.urns, this, this.logger);
111✔
224
                  }
111✔
225

189✔
226
                  try {
189✔
227
                    if (matches && rule.condition?.length) {
189✔
228
                      // context query is only checked when a rule exists
5✔
229
                      let context: any;
5✔
230
                      if (
5✔
231
                        this.resourceAdapter
5✔
232
                        && (
5✔
233
                          rule.context_query?.filters?.length
3✔
234
                          || rule.context_query?.query?.length
3!
235
                        )
5✔
236
                      ) {
5✔
237
                        context = await this.pullContextResources(rule.context_query, request);
2✔
238

2✔
239
                        if (_.isNil(context)) {
2!
240
                          this.logger.debug('Context query response is empty!');
×
241
                          return {  // deny by default
×
242
                            decision: Response_Decision.DENY,
×
243
                            obligations,
×
244
                            evaluation_cacheable,
×
245
                            operation_status: {
×
246
                              code: 200,
×
247
                              message: 'success'
×
248
                            }
×
249
                          };
×
250
                        }
×
251
                      }
2✔
252

5✔
253
                      request.context = context ?? request.context;
5✔
254
                      this.logger.debug('Validating rule condition', { name: rule.name, condition: rule.condition });
5✔
255
                      matches = conditionMatches(rule.condition, request);
5✔
256
                      this.logger.debug('condition validation response', { matches });
5✔
257
                    }
5✔
258
                  } catch (err: any) {
189!
UNCOV
259
                    this.logger.error('Caught an exception while applying rule condition to request', { code: err.code, message: err.message, stack: err.stack });
×
UNCOV
260
                    return {  // if an exception is caught deny by default
×
UNCOV
261
                      decision: Response_Decision.DENY,
×
UNCOV
262
                      obligations,
×
UNCOV
263
                      evaluation_cacheable,
×
UNCOV
264
                      operation_status: {
×
UNCOV
265
                        code: err.code ? err.code : 500,
×
UNCOV
266
                        message: err.message
×
UNCOV
267
                      }
×
UNCOV
268
                    };
×
UNCOV
269
                  }
×
270

189✔
271
                  // check if request has an ACL property set, if so verify it with the current rule target
189✔
272
                  if (matches && rule?.target) {
189✔
273
                    matches = await verifyACLList(rule.target, request, this.urns, this, this.logger);
100✔
274
                  }
100✔
275

189✔
276
                  if (matches && policySubjectMatch) {
189✔
277
                    if (!evaluationCacheableRule) {
172✔
278
                      evaluation_cacheable = evaluationCacheableRule;
172✔
279
                    }
172✔
280
                    ruleEffects.push({ effect: rule.effect, evaluation_cacheable });
172✔
281
                  }
172✔
282
                }
189✔
283
              }
404✔
284

145✔
285
              if (ruleEffects?.length > 0) {
145✔
286
                policyEffects.push(this.decide(policy.combining_algorithm, ruleEffects));
113✔
287
              }
113✔
288
            }
145✔
289
          }
146✔
290
        }
221✔
291

114✔
292
        if (policyEffects?.length > 0) {
114✔
293
          effect = this.decide(policySet.combining_algorithm, policyEffects);
100✔
294
        }
100✔
295
      }
114✔
296
    }
121✔
297

107✔
298
    if (!effect) {
107✔
299
      this.logger.silly('Access response is INDETERMINATE');
7✔
300
      return {
7✔
301
        decision: Response_Decision.INDETERMINATE,
7✔
302
        obligations,
7✔
303
        evaluation_cacheable: undefined,
7✔
304
        operation_status: {
7✔
305
          code: 200,
7✔
306
          message: 'success'
7✔
307
        }
7✔
308
      };
7✔
309
    }
7✔
310

100✔
311
    let decision: Response_Decision;
100✔
312
    decision = Response_Decision[effect.effect] || Response_Decision.INDETERMINATE;
107!
313

107✔
314
    this.logger.silly('Access response is', decision);
107✔
315
    return {
107✔
316
      decision,
107✔
317
      obligations,
107✔
318
      evaluation_cacheable: effect.evaluation_cacheable,
107✔
319
      operation_status: {
107✔
320
        code: 200,
107✔
321
        message: 'success'
107✔
322
      }
107✔
323
    };
107✔
324
  }
107✔
325

12✔
326
  async whatIsAllowed(request: Request): Promise<ReverseQuery> {
12✔
327
    let policySets: PolicySetRQ[] = [];
24✔
328
    let context = (request as any).context as ContextWithSubResolved;
24✔
329
    if (context?.subject?.token) {
24!
UNCOV
330
      const subject = await this.userService.findByToken({ token: context.subject.token });
×
UNCOV
331
      if (subject?.payload) {
×
UNCOV
332
        context.subject.id = subject.payload.id;
×
UNCOV
333
        (context.subject as any).tokens = subject.payload.tokens;
×
UNCOV
334
        context.subject.role_associations = subject.payload.role_associations;
×
UNCOV
335
      }
×
UNCOV
336
    }
×
337
    // check if context subject_id contains HR scope if not make request 'createHierarchicalScopes'
24✔
338
    if (context?.subject?.token &&
24!
339
      _.isEmpty(context.subject.hierarchical_scopes)) {
24!
UNCOV
340
      context = await this.createHRScope(context);
×
341
    }
×
342
    let obligations: Attribute[] = [];
24✔
343
    for (let [, value] of this.policySets) {
24✔
344
      let pSet: PolicySetRQ;
24✔
345
      if (
24✔
346
        _.isEmpty(value.target)
24✔
347
        || await this.targetMatches(value.target, request, 'whatIsAllowed', obligations)
24!
348
      ) {
24✔
349
        pSet = _.merge({}, { combining_algorithm: value.combining_algorithm }, _.pick(value, ['id', 'target', 'effect'])) as any;
24✔
350
        pSet.policies = [];
24✔
351

24✔
352
        let exactMatch = false;
24✔
353
        let policyEffect: Effect;
24✔
354
        for (let [, policy] of value.combinables) {
24✔
355
          if (policy.effect) {
37!
UNCOV
356
            policyEffect = policy.effect;
×
357
          } else if (policy?.combining_algorithm) {
37✔
358
            const method = this.combiningAlgorithms.get(policy.combining_algorithm);
37✔
359
            if (method === 'permitOverrides') {
37!
UNCOV
360
              policyEffect = Effect.PERMIT;
×
361
            } else if (method === 'denyOverrides') {
37!
UNCOV
362
              policyEffect = Effect.DENY;
×
363
            }
×
364
          }
37✔
365
          if (!!policy.target && await this.targetMatches(policy.target, request, 'whatIsAllowed', obligations, policyEffect)) {
37✔
366
            exactMatch = true;
10✔
367
            break;
10✔
368
          }
10✔
369
        }
37✔
370

24✔
371
        // if there are multiple entities in the request.target.resources
24✔
372
        // and if exactMatch is true, then check again with the resourcesAttributeMatch providing one entity each time
24✔
373
        // to ensure there is an exact policy entity match for each of the requested entity
24✔
374
        const entityURN = this.urns.get('entity');
24✔
375
        if (exactMatch && request?.target?.resources?.filter(att => att?.id === entityURN)?.length > 1) {
24✔
376
          exactMatch = this.checkMultipleEntitiesMatch(value, request, obligations);
10✔
377
        }
10✔
378

24✔
379
        for (let [, policy] of value.combinables) {
24✔
380
          let policyRQ: PolicyRQ;
40✔
381
          if (!policy) {
40!
UNCOV
382
            this.logger.debug('Policy Object not set');
×
UNCOV
383
            continue;
×
UNCOV
384
          }
×
385
          if (_.isEmpty(policy.target)
40✔
386
            || (exactMatch && await this.targetMatches(policy.target, request, 'whatIsAllowed', obligations, policyEffect))
40✔
387
            || (!exactMatch && await this.targetMatches(policy.target, request, 'whatIsAllowed', obligations, policyEffect, true))) {
40✔
388
            policyRQ = _.merge({}, { combining_algorithm: policy.combining_algorithm }, _.pick(policy, ['id', 'target', 'effect', 'evaluation_cacheable'])) as any;
34✔
389
            policyRQ.rules = [];
34✔
390

34✔
391
            policyRQ.has_rules = (!!policy.combinables && policy.combinables.size > 0);
34✔
392

34✔
393
            for (let [, rule] of policy.combinables) {
34✔
394
              if (!rule) {
116!
UNCOV
395
                this.logger.debug('Rule Object not set');
×
UNCOV
396
                continue;
×
UNCOV
397
              }
×
398
              let ruleRQ: RuleRQ;
116✔
399
              this.logger.debug(`WhatIsAllowed Checking rule target and request target for ${rule.name}`);
116✔
400
              let matches = _.isEmpty(rule.target) || await this.targetMatches(rule.target, request, 'whatIsAllowed', obligations, rule.effect);
116✔
401
              // check for regex if there is no direct match
94✔
402
              if (!matches) {
116✔
403
                matches = await this.targetMatches(rule.target, request, 'whatIsAllowed', obligations, rule.effect, true);
54✔
404
              }
54✔
405

116✔
406
              if (_.isEmpty(rule.target) || matches) {
116✔
407
                ruleRQ = _.merge({}, { context_query: rule.context_query }, _.pick(rule, ['id', 'target', 'effect', 'condition', 'evaluation_cacheable']));
62✔
408
                policyRQ.rules.push(ruleRQ);
62✔
409
              }
62✔
410
            }
116✔
411
            if (!!policyRQ.effect || (!policyRQ.effect && !_.isEmpty(policyRQ.rules))) {
34✔
412
              pSet.policies.push(policyRQ);
34✔
413
            }
34✔
414
          }
34✔
415
        }
40✔
416
        if (!_.isEmpty(pSet.policies)) {
24✔
417
          policySets.push(pSet);
24✔
418
        }
24✔
419
      }
24✔
420
    }
24✔
421
    return {
24✔
422
      policy_sets: policySets, obligations, operation_status: {
24✔
423
        code: 200,
24✔
424
        message: 'success'
24✔
425
      }
24✔
426
    };
24✔
427
  }
24✔
428

12✔
429
  private checkMultipleEntitiesMatch(policySet: PolicySetWithCombinables, request: Request, obligation: Attribute[]): boolean {
12✔
430
    let multipleEntitiesMatch = false;
23✔
431
    let exactMatch = true;
23✔
432
    // iterate and find for each of the exact mathing resource attribute
23✔
433
    const entityURN = this.urns.get('entity');
23✔
434
    for (let requestAttributeObj of request?.target?.resources || []) {
23!
435
      if (requestAttributeObj.id === entityURN) {
59✔
436
        multipleEntitiesMatch = false;
29✔
437
        for (let [, policyValue] of policySet.combinables) {
29✔
438
          const policy: Policy = policyValue;
58✔
439
          let policyEffect: Effect;
58✔
440
          if (policy.effect) {
58!
UNCOV
441
            policyEffect = policy.effect;
×
442
          } else if (policy.combining_algorithm) {
58✔
443
            const method = this.combiningAlgorithms.get(policy.combining_algorithm);
58✔
444
            if (method === 'permitOverrides') {
58!
UNCOV
445
              policyEffect = Effect.PERMIT;
×
446
            } else if (method === 'denyOverrides') {
58!
UNCOV
447
              policyEffect = Effect.DENY;
×
UNCOV
448
            }
×
449
          }
58✔
450
          if (policy?.target?.resources?.length > 0) {
58✔
451
            if (this.resourceAttributesMatch(policy?.target?.resources, [requestAttributeObj], 'isAllowed', obligation, policyEffect)) {
41✔
452
              multipleEntitiesMatch = true;
12✔
453
            }
12✔
454
          }
41✔
455
        }
58✔
456
        if (!multipleEntitiesMatch) {
29✔
457
          exactMatch = false; // reset exact match even if one of the multiple entities exact match is not found
17✔
458
          break;
17✔
459
        }
17✔
460
      }
29✔
461
    }
59✔
462
    return exactMatch;
23✔
463
  }
23✔
464

12✔
465
  private resourceAttributesMatch(ruleAttributes: Attribute[],
12✔
466
    requestAttributes: Attribute[], operation: AccessControlOperation,
468✔
467
    maskPropertyList: Attribute[], effect: Effect, regexMatch?: boolean): boolean {
468✔
468
    const entityURN = this.urns.get('entity');
468✔
469
    const propertyURN = this.urns.get('property');
468✔
470
    const maskedPropertyURN = this.urns.get('maskedProperty');
468✔
471
    const operationURN = this.urns.get('operation');
468✔
472
    let entityMatch = false;
468✔
473
    let propertyMatch = false;
468✔
474
    let rulePropertiesExist = false;
468✔
475
    let requestPropertiesExist = false;
468✔
476
    let operationMatch = false;
468✔
477
    let requestEntityURN = '';
468✔
478
    let skipDenyRule = true;
468✔
479
    let rulePropertyValue = '';
468✔
480
    // if there are no resources defined in rule or policy, return as resources match
468✔
481
    if (_.isEmpty(ruleAttributes)) {
468✔
482
      return true;
16✔
483
    }
16✔
484
    if (!maskPropertyList) {
468!
UNCOV
485
      maskPropertyList = [];
×
UNCOV
486
    }
×
487
    for (let reqAttr of requestAttributes || []) {
468!
488
      if (reqAttr.id === propertyURN) {
1,581✔
489
        requestPropertiesExist = true;
390✔
490
      }
390✔
491
    }
1,581✔
492
    for (let requestAttribute of requestAttributes || []) {
468!
493
      propertyMatch = false;
1,551✔
494
      for (let ruleAttribute of ruleAttributes || []) {
1,551!
495
        if (ruleAttribute.id === propertyURN) {
2,184✔
496
          rulePropertiesExist = true;
609✔
497
          rulePropertyValue = ruleAttribute.value;
609✔
498
        }
609✔
499
        // direct match for attribute values
2,184✔
500
        if (!regexMatch) {
2,184✔
501
          if (requestAttribute?.id === entityURN && ruleAttribute?.id === entityURN
1,793✔
502
            && requestAttribute?.value === ruleAttribute?.value) {
1,793✔
503
            // entity match
264✔
504
            entityMatch = true;
264✔
505
            requestEntityURN = requestAttribute.value;
264✔
506
          } else if (requestAttribute?.id === operationURN && ruleAttribute?.id === operationURN
1,793✔
507
            && requestAttribute?.value === ruleAttribute?.value) {
1,529✔
508
            operationMatch = true;
5✔
509
          } else if (entityMatch && requestAttribute?.id === propertyURN &&
1,529✔
510
            ruleAttribute?.id === propertyURN) {
1,524✔
511
            // check if requestEntityURNs entityName is part in the ruleAttribute property
180✔
512
            // if so check the rule attribute value and request attribute value, if not set property match for
180✔
513
            // this request property as true as it does not belong to this rule (since for multiple entities request
180✔
514
            // its possible that there could be properties from other entities)
180✔
515
            const entityName = requestEntityURN?.substring(requestEntityURN?.lastIndexOf(':') + 1);
180✔
516
            if (requestAttribute?.value?.indexOf(entityName) > -1) {
180✔
517
              // if match for request attribute is not found in rule attribute, Deny for isAllowed
134✔
518
              // and add properties to maskPropertyList for WhatIsAllowed
134✔
519
              if (ruleAttribute?.value === requestAttribute?.value) {
134✔
520
                propertyMatch = true;
51✔
521
              }
51✔
522
            } else if (effect === Effect.PERMIT) { // set propertyMatch to true only when rule is Permit and request does not belong to this rule
180✔
523
              propertyMatch = true; // the requested entity property does not belong to this rule
36✔
524
            }
36✔
525
          }
180✔
526
        } else if (regexMatch) {
2,184✔
527
          // regex match for attribute values
391✔
528
          if (requestAttribute?.id === entityURN && ruleAttribute?.id === entityURN) {
391✔
529
            // rule entity, get ruleNS and entityRegexValue for rule
93✔
530
            const value = ruleAttribute?.value;
93✔
531
            let pattern = value?.substring(value?.lastIndexOf(':') + 1);
93✔
532
            let nsEntityArray = pattern?.split('.');
93✔
533
            // firstElement could be either entity or namespace
93✔
534
            let nsOrEntity = nsEntityArray[0];
93✔
535
            let entityRegexValue = nsEntityArray[nsEntityArray?.length - 1];
93✔
536
            let reqNS, ruleNS;
93✔
537
            if (nsOrEntity?.toUpperCase() != entityRegexValue?.toUpperCase()) {
93!
UNCOV
538
              // rule name space is present
×
UNCOV
539
              ruleNS = nsOrEntity.toUpperCase();
×
UNCOV
540
            }
×
541

93✔
542
            // request entity, get reqNS and requestEntityValue for request
93✔
543
            let reqValue = requestAttribute?.value;
93✔
544
            requestEntityURN = reqValue;
93✔
545
            const reqAttributeNS = reqValue?.substring(0, reqValue?.lastIndexOf(':'));
93✔
546
            const ruleAttributeNS = value?.substring(0, value?.lastIndexOf(':'));
93✔
547
            // verify namespace before entity name
93✔
548
            if (reqAttributeNS != ruleAttributeNS) {
93!
UNCOV
549
              entityMatch = false;
×
UNCOV
550
            }
×
551
            let reqPattern = reqValue?.substring(reqValue?.lastIndexOf(':') + 1);
93✔
552
            let reqNSEntityArray = reqPattern?.split('.');
93✔
553
            // firstElement could be either entity or namespace
93✔
554
            let reqNSOrEntity = reqNSEntityArray[0];
93✔
555
            let requestEntityValue = reqNSEntityArray[reqNSEntityArray?.length - 1];
93✔
556
            if (reqNSOrEntity?.toUpperCase() != requestEntityValue?.toUpperCase()) {
93✔
557
              // request name space is present
3✔
558
              reqNS = reqNSOrEntity.toUpperCase();
3✔
559
            }
3✔
560

93✔
561
            if ((reqNS && ruleNS && (reqNS === ruleNS)) || (!reqNS && !ruleNS)) {
93!
562
              const reExp = new RegExp(entityRegexValue);
90✔
563
              if (requestEntityValue.match(reExp)) {
90✔
564
                entityMatch = true;
34✔
565
              }
34✔
566
            }
90✔
567
          } else if (entityMatch && requestAttribute?.id === propertyURN && ruleAttribute?.id === propertyURN) {
391✔
568
            // check for matching URN property value
37✔
569
            const rulePropertyValue = ruleAttribute?.value?.substring(ruleAttribute?.value?.lastIndexOf('#') + 1);
37✔
570
            const requestPropertyValue = requestAttribute?.value?.substring(requestAttribute?.value?.lastIndexOf('#') + 1);
37✔
571
            if (rulePropertyValue === requestPropertyValue) {
37✔
572
              propertyMatch = true;
8✔
573
            }
8✔
574
          }
37✔
575
        }
391✔
576
      }
2,184✔
577

1,551✔
578
      if (operation === 'isAllowed' && effect === Effect.DENY && (requestAttribute?.id === propertyURN || !requestPropertiesExist)
1,551✔
579
        && entityMatch && rulePropertiesExist && propertyMatch) {
1,551✔
580
        skipDenyRule = false; // Deny effect rule to be skipped only if the propertyMatch and effect is DENY
3✔
581
      }
3✔
582

1,551✔
583
      // if no match is found for the request attribute property in rule ==> this implies this is
1,551✔
584
      // an additional property in request which should be denied or masked
1,551✔
585
      if (operation === 'isAllowed' && effect === Effect.PERMIT && (requestAttribute?.id === propertyURN || !requestPropertiesExist)
1,551✔
586
        && entityMatch && rulePropertiesExist && !propertyMatch) {
1,551✔
587
        return false;
20✔
588
      }
20✔
589

1,531✔
590
      // for whatIsAllowed if decision is PERMIT and propertyMatch to false it implies
1,531✔
591
      // subject has requested additional properties requestAttribute.value add it to the maksPropertyList
1,531✔
592
      if (operation === 'whatIsAllowed' && effect === Effect.PERMIT && (requestAttribute?.id === propertyURN || !requestPropertiesExist)
1,551✔
593
        && entityMatch && rulePropertiesExist && !propertyMatch) {
1,551✔
594
        if (!requestPropertiesExist) {
9✔
595
          return false; // since its not possible to evaluate what properties subject would read
6✔
596
        }
6✔
597
        // since there can be multiple rules for same entity below check is to find if maskPropertyList already
3✔
598
        // contains the entityValue from previous matching rule
3✔
599
        const maskPropExists = maskPropertyList?.find((maskObj) => maskObj?.value === requestEntityURN);
9✔
600
        // for masking if no request properties are specified
9✔
601
        let maskProperty;
9✔
602
        if (requestPropertiesExist && requestAttribute?.value) {
9✔
603
          maskProperty = requestAttribute?.value;
3✔
604
        } else if (!requestPropertiesExist) {
9!
UNCOV
605
          maskProperty = rulePropertyValue;
×
UNCOV
606
        }
×
607
        if (maskProperty?.indexOf('#') <= -1) { // validate maskPropertyURN value
9!
UNCOV
608
          continue;
×
UNCOV
609
        }
×
610
        if (!maskPropExists) {
3✔
611
          maskPropertyList.push({ id: entityURN, value: requestEntityURN, attributes: [{ id: maskedPropertyURN, value: maskProperty, attributes: [] }] });
3✔
612
        } else {
9!
UNCOV
613
          maskPropExists.attributes.push({ id: maskedPropertyURN, value: maskProperty, attributes: [] });
×
614
        }
×
615
      }
9✔
616

1,525✔
617
      // for whatIsAllowed if decision is deny and propertyMatch to true it implies
1,525✔
618
      // subject does not have access to the requestAttribute.value add it to the maksPropertyList
1,525✔
619
      // last condition (propertyMatch || !requestPropertiesExist) -> is to match Deny rule when user does not provide any req props
1,525✔
620
      if (operation === 'whatIsAllowed' && effect === Effect.DENY && (requestAttribute?.id === propertyURN || !requestPropertiesExist)
1,551✔
621
        && entityMatch && rulePropertiesExist && (propertyMatch || !requestPropertiesExist)) {
1,551✔
622
        // since there can be multiple rules for same entity below check is to find if maskPropertyList already
11✔
623
        // contains the entityValue from previous matching rule
11✔
624
        const maskPropExists = maskPropertyList?.find((maskObj) => maskObj.value === requestEntityURN);
11✔
625
        let maskProperty;
11✔
626
        // for masking if no request properties are specified
11✔
627
        if (requestPropertiesExist && requestAttribute?.value) {
11✔
628
          maskProperty = requestAttribute.value;
3✔
629
        } else if (!requestPropertiesExist) {
11✔
630
          maskProperty = rulePropertyValue;
8✔
631
        }
8✔
632
        if (maskProperty?.indexOf('#') <= -1) { // validate maskPropertyURN value
11!
UNCOV
633
          continue;
×
UNCOV
634
        }
×
635
        if (!maskPropExists) {
11✔
636
          maskPropertyList.push({ id: entityURN, value: requestEntityURN, attributes: [{ id: maskedPropertyURN, value: maskProperty, attributes: [] }] });
6✔
637
        } else {
11✔
638
          maskPropExists.attributes.push({ id: maskedPropertyURN, value: maskProperty, attributes: [] });
5✔
639
        }
5✔
640
      }
11✔
641
    }
1,551✔
642

426✔
643
    // skip deny rule property is effective only if ruleProps exist and requestProps exist
426✔
644
    if (skipDenyRule && rulePropertiesExist && requestPropertiesExist && effect === Effect.DENY &&
468✔
645
      operation === 'isAllowed' && !propertyMatch) {
468✔
646
      return false;
8✔
647
    }
8✔
648

418✔
649
    // if there is no entity or no operation match return false
418✔
650
    if (!entityMatch && !operationMatch) {
468✔
651
      return false;
149✔
652
    }
149✔
653
    return true;
269✔
654
  }
269✔
655

12✔
656
  /**
12✔
657
 * Check if a request's target matches a rule, policy or policy set's target.
12✔
658
 * @param targetA
12✔
659
 * @param targetB
12✔
660
 */
12✔
661
  private async targetMatches(ruleTarget: Target, request: Request,
12✔
662
    operation: AccessControlOperation = 'isAllowed', maskPropertyList: Attribute[],
932✔
663
    effect: Effect = Effect.PERMIT, regexMatch?: boolean): Promise<boolean> {
932✔
664
    const requestTarget = request.target;
932✔
665
    const subMatch = await this.checkSubjectMatches(ruleTarget.subjects, requestTarget.subjects, request);
932✔
666
    const match = subMatch && this.attributesMatch(ruleTarget.actions, requestTarget.actions);
932✔
667
    if (!match) {
932✔
668
      return false;
505✔
669
    }
505✔
670
    return this.resourceAttributesMatch(ruleTarget.resources,
427✔
671
      requestTarget.resources, operation, maskPropertyList, effect, regexMatch);
427✔
672
  }
427✔
673

12✔
674
  /**
12✔
675
   * Check if the attributes of a action or resources from a rule, policy
12✔
676
   * or policy set match the attributes from a request.
12✔
677
   *
12✔
678
   * @param ruleAttributes
12✔
679
   * @param requestAttributes
12✔
680
   */
12✔
681
  private attributesMatch(ruleAttributes: Attribute[], requestAttributes: Attribute[]): boolean {
12✔
682
    for (let attribute of ruleAttributes || []) {
778✔
683
      const id = attribute?.id;
524✔
684
      const value = attribute?.value;
524✔
685
      const match = !!requestAttributes?.find((requestAttribute) => {
524✔
686
        // return requestAttribute.id == id && requestAttribute.value == value;
695✔
687
        if (requestAttribute?.id == id && requestAttribute?.value == value) {
695✔
688
          return true;
210✔
689
        } else {
695✔
690
          return false;
485✔
691
        }
485✔
692
      });
524✔
693

524✔
694
      if (!match) {
524✔
695
        return false;
314✔
696
      }
314✔
697
    }
524✔
698
    return true;
464✔
699
  }
464✔
700

12✔
701
  async getRedisKey(key: string): Promise<any> {
12✔
702
    if (!key) {
22!
703
      this.logger.info('Key not defined');
×
704
      return;
×
UNCOV
705
    }
×
706
    const redisResponse = await this.redisClient.get(key);
22✔
707
    if (!redisResponse) {
22✔
708
      this.logger.info('Key does not exist', { key });
2✔
709
      return;
2✔
710
    }
2✔
711
    if (redisResponse) {
20✔
712
      this.logger.debug('Found key in cache: ' + key);
20✔
713
      return JSON.parse(redisResponse);
20✔
714
    }
20✔
715
  }
22✔
716

12✔
717
  async evictHRScopes(subID: string): Promise<void> {
12✔
UNCOV
718
    const key = `cache:${subID}:*`;
×
UNCOV
719
    const matchingKeys = await this.redisClient.keys(key);
×
UNCOV
720
    await this.redisClient.del(matchingKeys);
×
UNCOV
721
    this.logger.debug('Evicted Subject cache: ' + key);
×
UNCOV
722
    return;
×
UNCOV
723
  }
×
724

12✔
725
  async setRedisKey(key: string, value: any): Promise<any> {
12✔
726
    if (!key || !value) {
4!
UNCOV
727
      this.logger.info(`Either key or value for redis set is not defined key: ${key} value: ${value}`);
×
UNCOV
728
      return;
×
729
    }
×
730
    return await this.redisClient.set(key, value);
4✔
731
  }
4✔
732

12✔
733
  async createHRScope(context: ContextWithSubResolved): Promise<ContextWithSubResolved> {
12✔
734
    if (context && !context.subject) {
20!
UNCOV
735
      context.subject = {};
×
UNCOV
736
    }
×
737
    const token = context.subject.token;
20✔
738
    const subjectID = context.subject.id;
20✔
739
    const subjectTokens = context.subject.tokens;
20✔
740
    const tokenFound = _.find(subjectTokens ?? [], { token });
20!
741
    let redisHRScopesKey;
20✔
742
    if (tokenFound?.interactive) {
20!
743
      redisHRScopesKey = `cache:${subjectID}:hrScopes`;
×
UNCOV
744
    }
×
745
    else if (tokenFound && !tokenFound.interactive) {
20✔
746
      redisHRScopesKey = `cache:${subjectID}:${token}:hrScopes`;
20✔
747
    }
20✔
UNCOV
748
    else {
×
UNCOV
749
      return context;
×
750
    }
×
751
    const timeout = this.cfg.get('authorization:hrReqTimeout') ?? 300000;
20✔
752
    const keyExist = await this.redisClient.exists(redisHRScopesKey);
20✔
753

20✔
754
    if (!keyExist) {
20✔
755
      const date = new Date().toISOString();
2✔
756
      const tokenDate = token + ':' + date;
2✔
757
      await this.userTopic.emit('hierarchicalScopesRequest', { token: tokenDate });
2✔
758
      this.waiting[tokenDate] = [];
2✔
759
      try {
2✔
760
        await new Promise((resolve, reject) => {
2✔
761
          const timeoutId = setTimeout(async () => {
2✔
UNCOV
762
            reject({ message: 'hr scope read timed out', tokenDate });
×
763
          }, timeout);
2✔
764
          this.waiting[tokenDate].push({ resolve, reject, timeoutId });
2✔
765
        });
2✔
766
        const subjectHRScopes = await this.getRedisKey(redisHRScopesKey);
2✔
767
        Object.assign(context.subject, { hierarchical_scopes: subjectHRScopes });
2✔
768
      } catch (err) {
2!
UNCOV
769
        // unhandled promise rejection for timeout
×
UNCOV
770
        this.logger.error(`Error creating Hierarchical scope for subject ${tokenDate}`);
×
UNCOV
771
      }
×
772
    } else {
20✔
773
      try {
18✔
774
        const subjectHRScopes = await this.getRedisKey(redisHRScopesKey);
18✔
775
        Object.assign(context.subject, { hierarchical_scopes: subjectHRScopes });
18✔
776
      } catch (err) {
18!
UNCOV
777
        this.logger.info(`Subject or HR Scope not persisted in redis in acs`);
×
UNCOV
778
      }
×
779
    }
18✔
780
    return context;
20✔
781
  }
20✔
782

12✔
783
  /**
12✔
784
   * Check if the Rule's Subject Role matches with atleast
12✔
785
   * one of the user role associations role value
12✔
786
   *
12✔
787
   * @param ruleAttributes
12✔
788
   * @param requestSubAttributes
12✔
789
   * @param request
12✔
790
   */
12✔
791
  private async checkSubjectMatches(ruleSubAttributes: Attribute[],
12✔
792
    requestSubAttributes: Attribute[], request: Request): Promise<boolean> {
932✔
793
    let context = (request as any)?.context as ContextWithSubResolved;
932✔
794
    // Just check the Role value matches here in subject
932✔
795
    const roleURN = this.urns.get('role');
932✔
796
    let ruleRole: string;
932✔
797
    if (!ruleSubAttributes || ruleSubAttributes.length === 0) {
932✔
798
      return true;
232✔
799
    }
232✔
800
    ruleSubAttributes?.forEach((subjectObject) => {
932✔
801
      if (subjectObject?.id === roleURN) {
1,154✔
802
        ruleRole = subjectObject?.value;
529✔
803
      }
529✔
804
    });
932✔
805

932✔
806
    // must be a rule subject targetted to specific user
932✔
807
    if (!ruleRole && this.attributesMatch(ruleSubAttributes, requestSubAttributes)) {
932✔
808
      this.logger.debug('Rule subject targetted to specific user', ruleSubAttributes);
37✔
809
      return true;
37✔
810
    }
37✔
811

663✔
812
    if (!ruleRole) {
932✔
813
      this.logger.warn(`Subject does not match with rule attributes`, ruleSubAttributes);
134✔
814
      return false;
134✔
815
    }
134✔
816
    if (!context?.subject?.role_associations) {
932✔
817
      this.logger.warn('Subject role associations missing', ruleSubAttributes);
8✔
818
      return false;
8✔
819
    }
8✔
820
    return context?.subject?.role_associations?.some((roleObj) => roleObj?.role === ruleRole);
932✔
821
  }
932✔
822

12✔
823
  /**
12✔
824
   * A list of rules or policies provides a list of Effects.
12✔
825
   * This method is invoked to evaluate the final effect
12✔
826
   * according to a combining algorithm
12✔
827
   * @param combiningAlgorithm
12✔
828
   * @param effects
12✔
829
   */
12✔
830
  private decide(combiningAlgorithm: string, effects: EffectEvaluation[]): EffectEvaluation {
12✔
831
    if (this.combiningAlgorithms.has(combiningAlgorithm)) {
213✔
832
      return this.combiningAlgorithms.get(combiningAlgorithm).apply(this, [effects]);
213✔
833
    }
213✔
UNCOV
834

×
UNCOV
835
    throw new errors.InvalidCombiningAlgorithm(combiningAlgorithm);
×
UNCOV
836
  }
×
837

12✔
838
  // Combining algorithms
12✔
839

12✔
840
  /**
12✔
841
  * Always DENY if DENY exists;
12✔
842
  * @param effects
12✔
843
  */
12✔
844
  protected denyOverrides(effects: EffectEvaluation[]): EffectEvaluation {
12✔
845
    let effect, evaluation_cacheable;
55✔
846
    for (let effectObj of effects || []) {
55!
847
      if (effectObj.effect === Effect.DENY) {
70✔
848
        effect = effectObj.effect;
26✔
849
        evaluation_cacheable = effectObj.evaluation_cacheable;
26✔
850
        break;
26✔
851
      } else {
70✔
852
        effect = effectObj.effect;
44✔
853
        evaluation_cacheable = effectObj.evaluation_cacheable;
44✔
854
      }
44✔
855
    }
70✔
856
    return {
55✔
857
      effect,
55✔
858
      evaluation_cacheable
55✔
859
    };
55✔
860
  }
55✔
861

12✔
862
  /**
12✔
863
   * Always PERMIT if PERMIT exists;
12✔
864
   * @param effects
12✔
865
   */
12✔
866
  protected permitOverrides(effects: EffectEvaluation[]): EffectEvaluation {
12✔
867
    let effect, evaluation_cacheable;
157✔
868
    for (let effectObj of effects || []) {
157!
869
      if (effectObj?.effect === Effect.PERMIT) {
160✔
870
        effect = effectObj.effect;
92✔
871
        evaluation_cacheable = effectObj.evaluation_cacheable;
92✔
872
        break;
92✔
873
      } else {
160✔
874
        effect = effectObj?.effect;
68✔
875
        evaluation_cacheable = effectObj.evaluation_cacheable;
68✔
876
      }
68✔
877
    }
160✔
878
    return {
157✔
879
      effect,
157✔
880
      evaluation_cacheable
157✔
881
    };
157✔
882
  }
157✔
883

12✔
884
  /**
12✔
885
   * Apply first effect which matches PERMIT or DENY.
12✔
886
   * Note that in a future implementation Effect may be extended to further values.
12✔
887
   * @param effects
12✔
888
   */
12✔
889
  protected firstApplicable(effects: EffectEvaluation[]): EffectEvaluation {
12✔
890
    return effects[0];
1✔
891
  }
1✔
892

12✔
893
  // in-memory resource handlers
12✔
894

12✔
895
  updatePolicySet(policySet: PolicySetWithCombinables): void {
12✔
896
    this.policySets.set(policySet.id, policySet);
25✔
897
  }
25✔
898

12✔
899
  removePolicySet(policySetID: string): void {
12✔
900
    this.policySets.delete(policySetID);
16✔
901
  }
16✔
902

12✔
903
  updatePolicy(policySetID: string, policy: PolicyWithCombinables): void {
12✔
904
    const policySet: PolicySetWithCombinables = this.policySets.get(policySetID);
25✔
905
    if (!_.isNil(policySet)) {
25✔
906
      policySet.combinables.set(policy.id, policy);
25✔
907
    }
25✔
908
  }
25✔
909

12✔
910
  removePolicy(policySetID: string, policyID: string): void {
12✔
911
    const policySet: PolicySetWithCombinables = this.policySets.get(policySetID);
1✔
912
    if (!_.isNil(policySet)) {
1✔
913
      policySet.combinables.delete(policyID);
1✔
914
    }
1✔
915
  }
1✔
916

12✔
917
  updateRule(policySetID: string, policyID: string, rule: Rule): void {
12✔
918
    const policySet: PolicySetWithCombinables = this.policySets.get(policySetID);
74✔
919
    if (!_.isNil(policySet)) {
74✔
920
      const policy: PolicyWithCombinables = policySet.combinables.get(policyID);
74✔
921
      if (!_.isNil(policy)) {
74✔
922
        policy.combinables.set(rule.id, rule);
74✔
923
      }
74✔
924
    }
74✔
925
  }
74✔
926

12✔
927
  removeRule(policySetID: string, policyID: string, ruleID: string): void {
12✔
928
    const policySet: PolicySetWithCombinables = this.policySets.get(policySetID);
3✔
929
    if (!_.isNil(policySet)) {
3✔
930
      const policy: PolicyWithCombinables = policySet.combinables.get(policyID);
3✔
931
      if (!_.isNil(policy)) {
3✔
932
        policy.combinables.delete(ruleID);
3✔
933
      }
3✔
934
    }
3✔
935
  }
3✔
936

12✔
937
  /**
12✔
938
   * Creates an adapter within the supported resource adapters.
12✔
939
   * @param adapterConfig
12✔
940
   */
12✔
941
  createResourceAdapter(adapterConfig: any): void {
12✔
942

6✔
943
    if (!_.isNil(adapterConfig.graphql)) {
6✔
944
      const opts = adapterConfig.graphql;
6✔
945
      this.resourceAdapter = new GraphQLAdapter(opts.url, this.logger, opts.clientOpts);
6✔
946
    } else {
6!
UNCOV
947
      throw new errors.UnsupportedResourceAdapter(adapterConfig);
×
UNCOV
948
    }
×
949
  }
6✔
950

12✔
951
  /**
12✔
952
   * Invokes adapter to pull necessary resources
12✔
953
   * and appends them to the request's context under the property `_queryResult`.
12✔
954
   * @param contextQuery A ContextQuery object.
12✔
955
   * @param context The request's context.
12✔
956
   */
12✔
957
  async pullContextResources(contextQuery: ContextQuery, request: Request): Promise<any> {
12✔
958
    const result = await this.resourceAdapter.query(contextQuery, request);
2✔
959

2✔
960
    return _.merge({}, context, {
2✔
961
      _queryResult: result
2✔
962
    });
2✔
963
  }
2✔
964
}
12✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc