@cloud-copilot/iam-lens 0.1.74 → 0.1.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/principalCan/permission.d.ts +50 -6
- package/dist/cjs/principalCan/permission.d.ts.map +1 -1
- package/dist/cjs/principalCan/permission.js +449 -353
- package/dist/cjs/principalCan/permission.js.map +1 -1
- package/dist/cjs/principalCan/permissionSet.d.ts.map +1 -1
- package/dist/cjs/principalCan/permissionSet.js +29 -11
- package/dist/cjs/principalCan/permissionSet.js.map +1 -1
- package/dist/cjs/principalCan/principalCan.d.ts.map +1 -1
- package/dist/cjs/principalCan/principalCan.js +87 -8
- package/dist/cjs/principalCan/principalCan.js.map +1 -1
- package/dist/cjs/principalCan/resources/resourceTypes/s3Buckets.d.ts +4 -0
- package/dist/cjs/principalCan/resources/resourceTypes/s3Buckets.d.ts.map +1 -1
- package/dist/cjs/principalCan/resources/resourceTypes/s3Buckets.js +59 -0
- package/dist/cjs/principalCan/resources/resourceTypes/s3Buckets.js.map +1 -1
- package/dist/cjs/principalCan/resources/statements.d.ts.map +1 -1
- package/dist/cjs/principalCan/resources/statements.js +3 -0
- package/dist/cjs/principalCan/resources/statements.js.map +1 -1
- package/dist/esm/principalCan/permission.d.ts +50 -6
- package/dist/esm/principalCan/permission.d.ts.map +1 -1
- package/dist/esm/principalCan/permission.js +446 -353
- package/dist/esm/principalCan/permission.js.map +1 -1
- package/dist/esm/principalCan/permissionSet.d.ts.map +1 -1
- package/dist/esm/principalCan/permissionSet.js +29 -11
- package/dist/esm/principalCan/permissionSet.js.map +1 -1
- package/dist/esm/principalCan/principalCan.d.ts.map +1 -1
- package/dist/esm/principalCan/principalCan.js +89 -10
- package/dist/esm/principalCan/principalCan.js.map +1 -1
- package/dist/esm/principalCan/resources/resourceTypes/s3Buckets.d.ts +4 -0
- package/dist/esm/principalCan/resources/resourceTypes/s3Buckets.d.ts.map +1 -1
- package/dist/esm/principalCan/resources/resourceTypes/s3Buckets.js +58 -0
- package/dist/esm/principalCan/resources/resourceTypes/s3Buckets.js.map +1 -1
- package/dist/esm/principalCan/resources/statements.d.ts.map +1 -1
- package/dist/esm/principalCan/resources/statements.js +3 -0
- package/dist/esm/principalCan/resources/statements.js.map +1 -1
- package/package.json +1 -1
|
@@ -173,7 +173,7 @@ export class Permission {
|
|
|
173
173
|
// 3. Attempt to combine conditions
|
|
174
174
|
const condsA = this.conditions || {};
|
|
175
175
|
const condsB = other.conditions || {};
|
|
176
|
-
const mergedConds =
|
|
176
|
+
const mergedConds = unionConditions(condsA, condsB);
|
|
177
177
|
if (mergedConds === null) {
|
|
178
178
|
return [this, other];
|
|
179
179
|
}
|
|
@@ -379,7 +379,13 @@ export class Permission {
|
|
|
379
379
|
}
|
|
380
380
|
/**
|
|
381
381
|
* Subtract a Deny permission from this Allow permission.
|
|
382
|
-
*
|
|
382
|
+
*
|
|
383
|
+
* Returns the resulting permissions, this can be:
|
|
384
|
+
* - An empty array if the Allow is fully denied by the Deny
|
|
385
|
+
* - A modified Allow permission or multiple Allow permissions
|
|
386
|
+
* - It could also return the original Allow and Deny permission if subtraction cannot be expressed purely in Allow statements
|
|
387
|
+
*
|
|
388
|
+
* @param other the Deny permission to subtract
|
|
383
389
|
*/
|
|
384
390
|
subtract(other) {
|
|
385
391
|
// Only subtract Deny from Allow for the same service/action
|
|
@@ -390,50 +396,9 @@ export class Permission {
|
|
|
390
396
|
// No subtraction applies
|
|
391
397
|
return [this];
|
|
392
398
|
}
|
|
393
|
-
// Early exit: identical conditions and deny covers allow resources => fully denied
|
|
394
399
|
const allowCondsNorm = normalizeConditionKeys(this.conditions || {});
|
|
395
400
|
const denyCondsNorm = normalizeConditionKeys(other.conditions || {});
|
|
396
|
-
|
|
397
|
-
// If both have resource[] and deny resources include all allow resources
|
|
398
|
-
if (this.resource && other.resource) {
|
|
399
|
-
if (this.resource.every((a) => other.resource.some((d) => wildcardToRegex(d).test(a)))) {
|
|
400
|
-
return [];
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
// If both have notResource[] and deny.notResource excludes superset of allow.notResource
|
|
404
|
-
if (this.notResource && other.notResource) {
|
|
405
|
-
// Deny excludes everything allow excludes or more, so allow has no effective resources
|
|
406
|
-
if (this.notResource.every((n) => other.notResource.includes(n))) {
|
|
407
|
-
return [];
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
|
-
// When Deny has multiple condition OPERATORS, we need to create separate Allow permissions
|
|
412
|
-
// for each inverted condition, since IAM can't express NOT(A AND B AND C) in one block.
|
|
413
|
-
// However, multiple keys under the same operator can be inverted together.
|
|
414
|
-
const denyConditions = other.conditions || {};
|
|
415
|
-
const hasMultipleConditionOperators = Object.keys(denyConditions).length > 1;
|
|
416
|
-
if (hasMultipleConditionOperators) {
|
|
417
|
-
// Special case: If deny resource is a proper subset of allow resource, keep both statements separate
|
|
418
|
-
// because it's impossible to merge them cleanly
|
|
419
|
-
if (this.resource && other.resource) {
|
|
420
|
-
const isDenySubsetOfAllow = other.resource.every((denyRes) => this.resource.some((allowRes) => allowRes !== denyRes && wildcardToRegex(allowRes).test(denyRes)));
|
|
421
|
-
if (isDenySubsetOfAllow) {
|
|
422
|
-
// Keep both the original allow and deny statements
|
|
423
|
-
return [this, other];
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
return this.subtractWithMultipleConditions(other);
|
|
427
|
-
}
|
|
428
|
-
// Handle simpler cases with single/no conditions using the existing logic
|
|
429
|
-
// 1. Invert Deny conditions
|
|
430
|
-
const inverted = invertConditions(denyConditions);
|
|
431
|
-
// 2. Merge conditions: original Allow ∧ inverted Deny
|
|
432
|
-
const allowConds = normalizeConditionKeys(this.conditions || {});
|
|
433
|
-
const mergedConds = mergeComplementaryConditions(mergeConditions(allowConds, inverted) || {
|
|
434
|
-
...allowConds,
|
|
435
|
-
...inverted
|
|
436
|
-
});
|
|
401
|
+
const conditionsMatch = JSON.stringify(allowCondsNorm) === JSON.stringify(denyCondsNorm);
|
|
437
402
|
const allowResource = this.resource;
|
|
438
403
|
const allowNotResource = this.notResource;
|
|
439
404
|
const denyResource = other.resource;
|
|
@@ -441,350 +406,303 @@ export class Permission {
|
|
|
441
406
|
const eff = this.effect;
|
|
442
407
|
const svc = this.service;
|
|
443
408
|
const act = this.action;
|
|
444
|
-
const conds = Object.keys(mergedConds).length ? mergedConds : undefined;
|
|
445
409
|
// Case: Allow.resource & Deny.resource
|
|
446
410
|
if (allowResource !== undefined && denyResource !== undefined) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
411
|
+
const overlappingResources = allowResource.some((a) => {
|
|
412
|
+
return denyResource.some((d) => {
|
|
413
|
+
return wildcardToRegex(d).test(a) || wildcardToRegex(a).test(d);
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
// If the resources in the allow and deny do not overlap, return the allow as is
|
|
417
|
+
if (!overlappingResources) {
|
|
418
|
+
return [this];
|
|
419
|
+
}
|
|
420
|
+
// Categories for allows, a single allow could be more than one, because the deny could have multiple
|
|
421
|
+
// Without Conditions:
|
|
422
|
+
//1. Exactly the same as a deny - remove the allow
|
|
423
|
+
//2. A subset of a deny - remove the allow
|
|
424
|
+
//3. A superset of a deny - keep the allow and the deny
|
|
425
|
+
//4. No overlap with any deny - keep the allow as is
|
|
426
|
+
//
|
|
427
|
+
// With Conditions:
|
|
428
|
+
//1. Exactly the same as a deny - invert the conditions and keep the allow
|
|
429
|
+
//2. A subset of a deny - invert the conditions and keep the allow
|
|
430
|
+
//3. A superset of a deny - keep the allow and the deny
|
|
431
|
+
//4. No overlap with any deny - keep the allow as is
|
|
432
|
+
const allowMatches = [];
|
|
433
|
+
const allowSubsets = [];
|
|
434
|
+
const allowSupersets = [];
|
|
435
|
+
const allowNoOverlap = [];
|
|
436
|
+
const denySubsets = [];
|
|
437
|
+
for (const allowedResource of allowResource) {
|
|
438
|
+
let isMatch = false;
|
|
439
|
+
let isSubset = false;
|
|
440
|
+
let isSuperset = false;
|
|
441
|
+
for (const deniedResource of denyResource) {
|
|
442
|
+
if (deniedResource === allowedResource) {
|
|
443
|
+
isMatch = true;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
if (wildcardToRegex(deniedResource).test(allowedResource)) {
|
|
447
|
+
isSubset = true;
|
|
448
|
+
break;
|
|
449
|
+
}
|
|
450
|
+
if (wildcardToRegex(allowedResource).test(deniedResource)) {
|
|
451
|
+
isSuperset = true;
|
|
452
|
+
denySubsets.push(deniedResource);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (isMatch) {
|
|
456
|
+
allowMatches.push(allowedResource);
|
|
454
457
|
}
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
|
|
458
|
+
else if (isSubset) {
|
|
459
|
+
allowSubsets.push(allowedResource);
|
|
460
|
+
}
|
|
461
|
+
else if (isSuperset) {
|
|
462
|
+
allowSupersets.push(allowedResource);
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
allowNoOverlap.push(allowedResource);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const permissionsToReturn = [];
|
|
469
|
+
if (allowNoOverlap.length > 0) {
|
|
470
|
+
permissionsToReturn.push(new Permission(eff, svc, act, allowNoOverlap, undefined, this.conditions));
|
|
471
|
+
}
|
|
472
|
+
if (allowSupersets.length > 0) {
|
|
473
|
+
permissionsToReturn.push(new Permission(eff, svc, act, allowSupersets, undefined, this.conditions));
|
|
474
|
+
}
|
|
475
|
+
if (allowMatches.length > 0 || allowSubsets.length > 0) {
|
|
476
|
+
// If conditions are identical, these are fully dropped from the Allow. If not, they need to be kept with inverted conditions
|
|
477
|
+
if (!conditionsMatch) {
|
|
478
|
+
const newAllow = new Permission(eff, svc, act, [...allowMatches, ...allowSubsets], undefined, this.conditions);
|
|
479
|
+
permissionsToReturn.push(...applyDenyConditionsToAllow(newAllow, other));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (denySubsets.length > 0) {
|
|
483
|
+
permissionsToReturn.push(new Permission('Deny', svc, act, denySubsets, undefined, other.conditions));
|
|
458
484
|
}
|
|
459
|
-
|
|
460
|
-
return [new Permission(eff, svc, act, allowResource, undefined, conds)];
|
|
485
|
+
return permissionsToReturn;
|
|
461
486
|
}
|
|
462
487
|
// Case: Allow.resource & Deny.notResource
|
|
463
|
-
//
|
|
464
|
-
//
|
|
488
|
+
// =======================================================================
|
|
489
|
+
// SEMANTICS:
|
|
490
|
+
// Deny.notResource means: "deny everything EXCEPT these patterns"
|
|
491
|
+
// So the deny APPLIES to resources that do NOT match denyNotResource patterns
|
|
492
|
+
// And the deny does NOT apply to resources that DO match denyNotResource patterns
|
|
493
|
+
// =======================================================================
|
|
465
494
|
if (allowResource !== undefined && denyNotResource !== undefined) {
|
|
466
|
-
//
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
|
|
470
|
-
//
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
495
|
+
// STEP 1: Categorize each allow resource based on relationship to denyNotResource patterns
|
|
496
|
+
//
|
|
497
|
+
// Categories:
|
|
498
|
+
// ExcludedFromDeny - Matches a denyNotResource pattern (deny doesn't apply to these)
|
|
499
|
+
// AffectedByDeny - Does NOT match any denyNotResource (deny applies to these)
|
|
500
|
+
// Superset - Covers (is broader than) a denyNotResource pattern
|
|
501
|
+
//
|
|
502
|
+
// Also track which denyNotResource patterns are covered by superset allow resources
|
|
503
|
+
const excludedFromDeny = [];
|
|
504
|
+
const affectedByDeny = [];
|
|
505
|
+
const supersets = [];
|
|
506
|
+
const coveredDenyNotResourcePatterns = [];
|
|
507
|
+
for (const allowedResource of allowResource) {
|
|
508
|
+
let isExcluded = false;
|
|
509
|
+
let isSuperset = false;
|
|
510
|
+
for (const deniedNotResource of denyNotResource) {
|
|
511
|
+
// Check if allowResource exactly matches or is covered by denyNotResource pattern
|
|
512
|
+
if (allowedResource === deniedNotResource ||
|
|
513
|
+
wildcardToRegex(deniedNotResource).test(allowedResource)) {
|
|
514
|
+
isExcluded = true;
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
// Check if allowResource covers (is broader than) denyNotResource pattern
|
|
518
|
+
if (wildcardToRegex(allowedResource).test(deniedNotResource)) {
|
|
519
|
+
isSuperset = true;
|
|
520
|
+
if (!coveredDenyNotResourcePatterns.includes(deniedNotResource)) {
|
|
521
|
+
coveredDenyNotResourcePatterns.push(deniedNotResource);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (isExcluded) {
|
|
526
|
+
excludedFromDeny.push(allowedResource);
|
|
527
|
+
}
|
|
528
|
+
else if (isSuperset) {
|
|
529
|
+
supersets.push(allowedResource);
|
|
491
530
|
}
|
|
492
531
|
else {
|
|
493
|
-
|
|
532
|
+
affectedByDeny.push(allowedResource);
|
|
494
533
|
}
|
|
495
534
|
}
|
|
496
|
-
//
|
|
497
|
-
if (
|
|
498
|
-
|
|
499
|
-
// Only resources excluded from deny survive
|
|
500
|
-
// If allow.resource patterns are broader than deny.notResource,
|
|
501
|
-
// narrow down to the deny.notResource patterns themselves
|
|
502
|
-
if (resourcesExcludedFromDeny.length === 0 && resourcesAffectedByDeny.length > 0) {
|
|
503
|
-
// Check if any allow resource is a superset of deny.notResource patterns
|
|
504
|
-
const narrowedResources = denyNotResource.filter((dnr) => allowResource.some((ar) => wildcardToRegex(ar).test(dnr)));
|
|
505
|
-
if (narrowedResources.length > 0) {
|
|
506
|
-
return [new Permission(eff, svc, act, narrowedResources, undefined, conds)];
|
|
507
|
-
}
|
|
508
|
-
return [];
|
|
509
|
-
}
|
|
510
|
-
if (resourcesExcludedFromDeny.length === 0)
|
|
511
|
-
return [];
|
|
512
|
-
return [new Permission(eff, svc, act, resourcesExcludedFromDeny, undefined, conds)];
|
|
535
|
+
// STEP 2: Early exit - if all allow resources are excluded from deny, return unchanged
|
|
536
|
+
if (excludedFromDeny.length === allowResource.length) {
|
|
537
|
+
return [this];
|
|
513
538
|
}
|
|
514
|
-
|
|
515
|
-
//
|
|
516
|
-
|
|
517
|
-
//
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
return false;
|
|
521
|
-
return Object.entries(keyMap).every(([key, denyVals]) => {
|
|
522
|
-
const allowVals = allowCondsNorm[op]?.[key];
|
|
523
|
-
if (!allowVals)
|
|
524
|
-
return false;
|
|
525
|
-
// For StringEquals-like operators, all deny values must be in allow values
|
|
526
|
-
return denyVals.every((dv) => allowVals.includes(dv));
|
|
527
|
-
});
|
|
528
|
-
});
|
|
529
|
-
// Check if allow conditions are fully contained within deny conditions
|
|
530
|
-
const allowIsSubsetOfDeny = Object.entries(allowCondsNorm).every(([op, keyMap]) => {
|
|
531
|
-
if (!denyCondsNorm[op])
|
|
532
|
-
return false;
|
|
533
|
-
return Object.entries(keyMap).every(([key, allowVals]) => {
|
|
534
|
-
const denyVals = denyCondsNorm[op]?.[key];
|
|
535
|
-
if (!denyVals)
|
|
536
|
-
return false;
|
|
537
|
-
return allowVals.every((av) => denyVals.includes(av));
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
// Case 1: Identical conditions
|
|
541
|
-
if (conditionsMatch) {
|
|
542
|
-
// Resources affected by deny are fully denied
|
|
543
|
-
// Narrow to resources excluded from deny (or notResource patterns if broader)
|
|
544
|
-
if (resourcesExcludedFromDeny.length > 0) {
|
|
545
|
-
return [
|
|
546
|
-
new Permission(eff, svc, act, resourcesExcludedFromDeny, undefined, this.conditions)
|
|
547
|
-
];
|
|
548
|
-
}
|
|
549
|
-
// If all resources affected by deny, narrow to notResource patterns if possible
|
|
550
|
-
const narrowedResources = denyNotResource.filter((dnr) => allowResource.some((ar) => wildcardToRegex(ar).test(dnr)));
|
|
551
|
-
if (narrowedResources.length > 0) {
|
|
552
|
-
return [new Permission(eff, svc, act, narrowedResources, undefined, this.conditions)];
|
|
553
|
-
}
|
|
554
|
-
return [];
|
|
539
|
+
const denyHasConditions = other.conditions && Object.keys(other.conditions).length > 0;
|
|
540
|
+
// STEP 3: Build output permissions by category
|
|
541
|
+
const permissionsToReturn = [];
|
|
542
|
+
// ExcludedFromDeny: Keep as-is with original conditions (deny doesn't touch these)
|
|
543
|
+
if (excludedFromDeny.length > 0) {
|
|
544
|
+
permissionsToReturn.push(new Permission(eff, svc, act, excludedFromDeny, undefined, this.conditions));
|
|
555
545
|
}
|
|
556
|
-
//
|
|
557
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
//
|
|
561
|
-
if (
|
|
562
|
-
|
|
563
|
-
return [new Permission(eff, svc, act, allowResource, undefined, this.conditions)];
|
|
564
|
-
}
|
|
565
|
-
// If allow has no conditions, only resourcesExcludedFromDeny can survive
|
|
566
|
-
// For resources affected by deny with inverted conditions
|
|
567
|
-
if (Object.keys(allowCondsNorm).length === 0) {
|
|
568
|
-
const results = [];
|
|
569
|
-
if (resourcesExcludedFromDeny.length > 0) {
|
|
570
|
-
results.push(new Permission(eff, svc, act, resourcesExcludedFromDeny, undefined, this.conditions));
|
|
571
|
-
}
|
|
572
|
-
if (resourcesAffectedByDeny.length > 0) {
|
|
573
|
-
// Can only survive with inverted deny conditions
|
|
574
|
-
results.push(new Permission(eff, svc, act, resourcesAffectedByDeny, undefined, conds));
|
|
575
|
-
}
|
|
576
|
-
return results.length > 0 ? results : [];
|
|
546
|
+
// Superset: Allow resource is broader than denyNotResource patterns
|
|
547
|
+
// - The covered denyNotResource patterns are excluded from deny (allow unconditionally)
|
|
548
|
+
// - The superset allow resources are affected by deny (apply inverted conditions)
|
|
549
|
+
if (supersets.length > 0) {
|
|
550
|
+
// First: Allow the covered patterns unconditionally (they're excluded from deny)
|
|
551
|
+
if (coveredDenyNotResourcePatterns.length > 0) {
|
|
552
|
+
permissionsToReturn.push(new Permission(eff, svc, act, coveredDenyNotResourcePatterns, undefined, this.conditions));
|
|
577
553
|
}
|
|
578
|
-
//
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
new Permission(eff, svc, act, resourcesExcludedFromDeny, undefined, this.conditions)
|
|
583
|
-
];
|
|
554
|
+
// Second: Apply inverted deny conditions to the superset resources
|
|
555
|
+
if (denyHasConditions && !conditionsMatch) {
|
|
556
|
+
const supersetAllow = new Permission(eff, svc, act, supersets, undefined, this.conditions);
|
|
557
|
+
permissionsToReturn.push(...applyDenyConditionsToAllow(supersetAllow, other));
|
|
584
558
|
}
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
559
|
+
// If no conditions or conditions match, the superset is fully denied (nothing to add)
|
|
560
|
+
}
|
|
561
|
+
// AffectedByDeny: These resources are hit by the deny
|
|
562
|
+
if (affectedByDeny.length > 0) {
|
|
563
|
+
// If there are no conditions on deny - these are fully denied (drop them)
|
|
564
|
+
// If the conditions match - these are fully denied (drop them)
|
|
565
|
+
if (denyHasConditions && !conditionsMatch) {
|
|
566
|
+
// Different conditions - keep with inverted deny conditions
|
|
567
|
+
const newAllow = new Permission(eff, svc, act, affectedByDeny, undefined, this.conditions);
|
|
568
|
+
permissionsToReturn.push(...applyDenyConditionsToAllow(newAllow, other));
|
|
589
569
|
}
|
|
590
|
-
// All resources affected by deny and deny conditions cover all allow conditions
|
|
591
|
-
return [];
|
|
592
570
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
if (!denyCoversAllMatchingValues)
|
|
571
|
+
return permissionsToReturn;
|
|
572
|
+
}
|
|
573
|
+
// Scenario 3: Allow.notResource & Deny.resource
|
|
574
|
+
if (allowNotResource !== undefined && denyResource !== undefined) {
|
|
575
|
+
// STEP 1: Categorize relationships and track which denyResources are already covered
|
|
576
|
+
const coveredDenyResources = new Set(); // denyResources already excluded by allowNotResource
|
|
577
|
+
const uncoveredDenyResources = []; // denyResources that affect allowed resources
|
|
578
|
+
const subsetReplacements = [];
|
|
579
|
+
// For each denyResource, check if it's covered by any allowNotResource
|
|
580
|
+
for (const denyPattern of denyResource) {
|
|
581
|
+
let isCovered = false;
|
|
582
|
+
for (const allowPattern of allowNotResource) {
|
|
583
|
+
// ExactMatch or Superset - denyResource is already excluded
|
|
584
|
+
if (allowPattern === denyPattern || wildcardToRegex(allowPattern).test(denyPattern)) {
|
|
585
|
+
isCovered = true;
|
|
586
|
+
coveredDenyResources.add(denyPattern);
|
|
612
587
|
break;
|
|
613
|
-
}
|
|
614
|
-
if (denyCoversAllMatchingValues) {
|
|
615
|
-
// Deny covers all matching values, so it's a full deny for affected resources
|
|
616
|
-
if (resourcesExcludedFromDeny.length > 0) {
|
|
617
|
-
return [
|
|
618
|
-
new Permission(eff, svc, act, resourcesExcludedFromDeny, undefined, this.conditions)
|
|
619
|
-
];
|
|
620
588
|
}
|
|
621
|
-
return [];
|
|
622
589
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const results = [];
|
|
626
|
-
// Resources excluded from deny always survive with original conditions
|
|
627
|
-
if (resourcesExcludedFromDeny.length > 0) {
|
|
628
|
-
results.push(new Permission(eff, svc, act, resourcesExcludedFromDeny, undefined, this.conditions));
|
|
590
|
+
if (!isCovered) {
|
|
591
|
+
uncoveredDenyResources.push(denyPattern);
|
|
629
592
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
const denyVals = denyCondsNorm[op]?.[key];
|
|
637
|
-
if (denyVals) {
|
|
638
|
-
// Subtract deny values from allow values
|
|
639
|
-
const remaining = allowVals.filter((av) => !denyVals.includes(av));
|
|
640
|
-
if (remaining.length > 0) {
|
|
641
|
-
if (!remainingConds[op])
|
|
642
|
-
remainingConds[op] = {};
|
|
643
|
-
remainingConds[op][key] = remaining;
|
|
644
|
-
hasRemainingConditions = true;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
// Keep as is
|
|
649
|
-
if (!remainingConds[op])
|
|
650
|
-
remainingConds[op] = {};
|
|
651
|
-
remainingConds[op][key] = allowVals;
|
|
652
|
-
hasRemainingConditions = true;
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
if (hasRemainingConditions) {
|
|
657
|
-
results.push(new Permission(eff, svc, act, resourcesAffectedByDeny, undefined, remainingConds));
|
|
593
|
+
}
|
|
594
|
+
// Check for Subset patterns (denyResource covers allowNotResource)
|
|
595
|
+
for (const allowPattern of allowNotResource) {
|
|
596
|
+
for (const denyPattern of denyResource) {
|
|
597
|
+
if (wildcardToRegex(denyPattern).test(allowPattern) && allowPattern !== denyPattern) {
|
|
598
|
+
subsetReplacements.push({ allowPattern, denyPattern });
|
|
658
599
|
}
|
|
659
600
|
}
|
|
660
|
-
return results;
|
|
661
601
|
}
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
602
|
+
// Filter out subset deny patterns from uncoveredDenyResources to get true NoOverlap patterns
|
|
603
|
+
const subsetDenyPatternsSet = new Set(subsetReplacements.map((s) => s.denyPattern));
|
|
604
|
+
const noOverlapDenyResources = uncoveredDenyResources.filter((dr) => !subsetDenyPatternsSet.has(dr));
|
|
605
|
+
// STEP 2: If all denyResources are covered, deny has no effect
|
|
606
|
+
if (noOverlapDenyResources.length === 0 && subsetReplacements.length === 0) {
|
|
607
|
+
return [this];
|
|
668
608
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
609
|
+
// STEP 3: Build output permissions
|
|
610
|
+
const denyHasConditions = other.conditions && Object.keys(other.conditions).length > 0;
|
|
611
|
+
// Build the expanded notResource (original + noOverlap deny resources + subset replacements)
|
|
612
|
+
const subsetAllowPatterns = new Set(subsetReplacements.map((s) => s.allowPattern));
|
|
613
|
+
const keptPatterns = allowNotResource.filter((p) => !subsetAllowPatterns.has(p));
|
|
614
|
+
const subsetDenyPatterns = Array.from(new Set(subsetReplacements.map((s) => s.denyPattern)));
|
|
615
|
+
const expandedNotResource = Array.from(new Set([...keptPatterns, ...subsetDenyPatterns, ...noOverlapDenyResources]));
|
|
616
|
+
// Same conditions or no deny conditions: simply expand notResource
|
|
617
|
+
if (conditionsMatch || !denyHasConditions) {
|
|
618
|
+
return [new Permission(eff, svc, act, undefined, expandedNotResource, this.conditions)];
|
|
672
619
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
//
|
|
678
|
-
|
|
679
|
-
|
|
620
|
+
// Different conditions: handle Subset and NoOverlap cases separately
|
|
621
|
+
const permissionsToReturn = [];
|
|
622
|
+
const hasSubsetReplacements = subsetReplacements.length > 0;
|
|
623
|
+
const hasNoOverlapAdditions = noOverlapDenyResources.length > 0;
|
|
624
|
+
// Part 1: Original allowNotResource with inverted deny conditions
|
|
625
|
+
// (when deny condition is NOT met, original allow applies)
|
|
626
|
+
const originalAllow = new Permission(eff, svc, act, undefined, allowNotResource, this.conditions);
|
|
627
|
+
permissionsToReturn.push(...applyDenyConditionsToAllow(originalAllow, other));
|
|
628
|
+
// Part 2a: For SUBSET replacements - expanded notResource WITH deny conditions
|
|
629
|
+
// (replacing smaller exclusion with larger one, only valid when condition is met)
|
|
630
|
+
if (hasSubsetReplacements) {
|
|
631
|
+
// Build notResource with just the subset replacements (not the noOverlap additions)
|
|
632
|
+
const subsetExpandedNotResource = Array.from(new Set([...keptPatterns, ...subsetDenyPatterns]));
|
|
633
|
+
const subsetConditions = intersectConditions(this.conditions || {}, other.conditions || {});
|
|
634
|
+
permissionsToReturn.push(new Permission(eff, svc, act, undefined, subsetExpandedNotResource, subsetConditions || other.conditions));
|
|
680
635
|
}
|
|
681
|
-
//
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
return [new Permission(eff, svc, act, undefined, allowNotResource, conds)];
|
|
636
|
+
// Part 2b: For NO OVERLAP additions - expanded notResource WITHOUT conditions
|
|
637
|
+
// (adding new exclusion is always safe, no condition needed)
|
|
638
|
+
if (hasNoOverlapAdditions) {
|
|
639
|
+
permissionsToReturn.push(new Permission(eff, svc, act, undefined, expandedNotResource, this.conditions));
|
|
686
640
|
}
|
|
687
|
-
|
|
688
|
-
const newNot = Array.from(new Set([...allowNotResource, ...denyResource]));
|
|
689
|
-
return [new Permission(eff, svc, act, undefined, newNot, conds)];
|
|
641
|
+
return permissionsToReturn;
|
|
690
642
|
}
|
|
691
|
-
//
|
|
643
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
644
|
+
// SCENARIO 4: Allow.NotResource & Deny.NotResource
|
|
645
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
646
|
+
//
|
|
647
|
+
// Semantics:
|
|
648
|
+
// Allow.notResource = [A]: allow ALL resources EXCEPT those matching A
|
|
649
|
+
// Deny.notResource = [D]: deny ALL resources EXCEPT those matching D
|
|
650
|
+
//
|
|
651
|
+
// The deny blocks everything except D (the "safe zone").
|
|
652
|
+
// The allow permits everything except A.
|
|
653
|
+
//
|
|
654
|
+
// Surviving resources = (allowed) ∩ (not denied)
|
|
655
|
+
// = (NOT in A) ∩ (in D)
|
|
656
|
+
// = resources in D that are not covered by A
|
|
657
|
+
//
|
|
658
|
+
// Result: resource: [D patterns not covered by A]
|
|
659
|
+
//
|
|
660
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
692
661
|
if (allowNotResource !== undefined && denyNotResource !== undefined) {
|
|
693
|
-
|
|
694
|
-
if
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
throw new Error('Permission.subtract: This should never happen—invalid state.');
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Handle subtraction when the Deny permission has multiple conditions.
|
|
707
|
-
* Creates separate Allow permissions for each inverted condition.
|
|
708
|
-
*/
|
|
709
|
-
subtractWithMultipleConditions(other) {
|
|
710
|
-
const allowResource = this.resource;
|
|
711
|
-
const allowNotResource = this.notResource;
|
|
712
|
-
const denyResource = other.resource;
|
|
713
|
-
const denyNotResource = other.notResource;
|
|
714
|
-
const eff = this.effect;
|
|
715
|
-
const svc = this.service;
|
|
716
|
-
const act = this.action;
|
|
717
|
-
// Determine resulting resource/notResource based on Allow vs Deny patterns
|
|
718
|
-
let resultResource;
|
|
719
|
-
let resultNotResource;
|
|
720
|
-
// Case: Allow.resource & Deny.notResource --> The Deny excludes everything EXCEPT notResource
|
|
721
|
-
// So the intersection is the items from Deny.notResource that match Allow.resource patterns
|
|
722
|
-
if (allowResource !== undefined && denyNotResource !== undefined) {
|
|
723
|
-
resultResource = denyNotResource.filter((dnr) => allowResource.some((ar) => wildcardToRegex(ar).test(dnr)));
|
|
724
|
-
if (resultResource.length === 0)
|
|
662
|
+
const denyHasConditions = other.conditions && Object.keys(other.conditions).length > 0;
|
|
663
|
+
// Helper: Check if pattern A covers pattern B (A is superset of or equal to B)
|
|
664
|
+
const patternCovers = (a, b) => {
|
|
665
|
+
return a === b || wildcardToRegex(a).test(b);
|
|
666
|
+
};
|
|
667
|
+
// Find D patterns that survive (not covered by any A pattern)
|
|
668
|
+
// These are the resources that are both allowed AND protected from deny
|
|
669
|
+
const survivingResources = denyNotResource.filter((dPattern) => !allowNotResource.some((aPattern) => patternCovers(aPattern, dPattern)));
|
|
670
|
+
// If nothing survives, return empty
|
|
671
|
+
if (survivingResources.length === 0) {
|
|
725
672
|
return [];
|
|
726
|
-
}
|
|
727
|
-
// Case: Allow.notResource & Deny.resource --> resultNotResource = ANR ∪ DR
|
|
728
|
-
else if (allowNotResource !== undefined && denyResource !== undefined) {
|
|
729
|
-
// Check if every Deny resource is already excluded by allowNotResource
|
|
730
|
-
const denyCovered = denyResource.every((dr) => allowNotResource.some((anr) => wildcardToRegex(anr).test(dr)));
|
|
731
|
-
if (denyCovered) {
|
|
732
|
-
resultNotResource = allowNotResource;
|
|
733
673
|
}
|
|
734
|
-
|
|
735
|
-
|
|
674
|
+
// Handle conditions
|
|
675
|
+
if (!denyHasConditions || conditionsMatch) {
|
|
676
|
+
// No deny conditions or same conditions: apply directly
|
|
677
|
+
return [new Permission(eff, svc, act, survivingResources, undefined, this.conditions)];
|
|
736
678
|
}
|
|
679
|
+
// Different conditions: split into two parts
|
|
680
|
+
const permissionsToReturn = [];
|
|
681
|
+
// Part 1: When deny condition is NOT met → original allow applies
|
|
682
|
+
const originalAllow = new Permission(eff, svc, act, undefined, allowNotResource, this.conditions);
|
|
683
|
+
permissionsToReturn.push(...applyDenyConditionsToAllow(originalAllow, other));
|
|
684
|
+
// Part 2: When deny condition IS met → surviving resources with deny's condition
|
|
685
|
+
const denyConditionCount = Object.values(other.conditions || {}).reduce((sum, keyMap) => sum + Object.keys(keyMap).length, 0);
|
|
686
|
+
const part2Conditions = denyConditionCount === 1 ? other.conditions : undefined;
|
|
687
|
+
permissionsToReturn.push(new Permission(eff, svc, act, survivingResources, undefined, part2Conditions));
|
|
688
|
+
return permissionsToReturn;
|
|
737
689
|
}
|
|
738
|
-
//
|
|
739
|
-
|
|
740
|
-
resultResource = allowResource;
|
|
741
|
-
}
|
|
742
|
-
// Case: Allow.notResource & Deny.notResource --> keep allow notResource, conditions handle exclusion
|
|
743
|
-
else if (allowNotResource !== undefined && denyNotResource !== undefined) {
|
|
744
|
-
resultNotResource = allowNotResource;
|
|
745
|
-
}
|
|
746
|
-
// Create separate Allow permissions for each condition key-value pair in Deny
|
|
747
|
-
const results = [];
|
|
748
|
-
const allowConds = normalizeConditionKeys(this.conditions || {});
|
|
749
|
-
for (const [operator, keyMap] of Object.entries(other.conditions || {})) {
|
|
750
|
-
for (const [contextKey, values] of Object.entries(keyMap)) {
|
|
751
|
-
// Invert this specific condition
|
|
752
|
-
const singleCondition = { [operator]: { [contextKey]: values } };
|
|
753
|
-
const invertedCondition = invertConditions(singleCondition);
|
|
754
|
-
// Merge with the original Allow conditions
|
|
755
|
-
const mergedConds = { ...allowConds };
|
|
756
|
-
for (const [invertedOp, invertedKeyMap] of Object.entries(invertedCondition)) {
|
|
757
|
-
if (mergedConds[invertedOp]) {
|
|
758
|
-
// Merge keys under the same operator
|
|
759
|
-
for (const [invertedKey, invertedVals] of Object.entries(invertedKeyMap)) {
|
|
760
|
-
if (mergedConds[invertedOp][invertedKey]) {
|
|
761
|
-
// Union the values
|
|
762
|
-
mergedConds[invertedOp][invertedKey] = Array.from(new Set([...mergedConds[invertedOp][invertedKey], ...invertedVals]));
|
|
763
|
-
}
|
|
764
|
-
else {
|
|
765
|
-
mergedConds[invertedOp][invertedKey] = Array.from(invertedVals);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
else {
|
|
770
|
-
mergedConds[invertedOp] = { ...invertedKeyMap };
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
// Apply complementary condition logic
|
|
774
|
-
const finalMergedConds = mergeComplementaryConditions(mergedConds);
|
|
775
|
-
const finalConds = Object.keys(finalMergedConds).length ? finalMergedConds : undefined;
|
|
776
|
-
// Create a new Allow permission with this specific inverted condition
|
|
777
|
-
results.push(new Permission(eff, svc, act, resultResource, resultNotResource, finalConds));
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
return results;
|
|
690
|
+
// This should never happen
|
|
691
|
+
throw new Error('Permission.subtract: This should never happen—invalid state.');
|
|
781
692
|
}
|
|
782
693
|
}
|
|
783
694
|
/**
|
|
784
|
-
* Attempt to
|
|
785
|
-
*
|
|
695
|
+
* Attempt to union two sets of permission conditions.
|
|
696
|
+
*
|
|
697
|
+
* If the conditions can be merged into a single block that allows all cases allowed by either,
|
|
698
|
+
* returns the merged conditions. If they cannot be merged cleanly (e.g., differing operators
|
|
699
|
+
* or incompatible numeric boundaries), returns null.
|
|
700
|
+
*
|
|
701
|
+
* @param a First set of conditions
|
|
702
|
+
* @param b Second set of conditions
|
|
703
|
+
* @returns Merged conditions or null if they cannot be merged
|
|
786
704
|
*/
|
|
787
|
-
function
|
|
705
|
+
export function unionConditions(a, b) {
|
|
788
706
|
// 1. If the set of operators in 'a' differs from the set in 'b', return null.
|
|
789
707
|
a = normalizeConditionKeys(a);
|
|
790
708
|
b = normalizeConditionKeys(b);
|
|
@@ -894,6 +812,142 @@ function mergeConditions(a, b) {
|
|
|
894
812
|
}
|
|
895
813
|
return merged;
|
|
896
814
|
}
|
|
815
|
+
/**
|
|
816
|
+
* Intersect two sets of permission conditions.
|
|
817
|
+
*
|
|
818
|
+
* Attempt to find the intersection of two sets of IAM condition clauses. This will
|
|
819
|
+
* combine condition operators and context keys, retaining only values that satisfy
|
|
820
|
+
* both sets of conditions. If the intersection is empty or cannot be expressed
|
|
821
|
+
* cleanly, returns null.
|
|
822
|
+
*
|
|
823
|
+
* @param conditionsA First set of conditions
|
|
824
|
+
* @param conditionsB Second set of conditions
|
|
825
|
+
* @returns Intersected conditions or null if intersection is empty or cannot be expressed
|
|
826
|
+
*/
|
|
827
|
+
export function intersectConditions(a, b) {
|
|
828
|
+
// Normalize both condition sets to lowercase operators and keys
|
|
829
|
+
const normalizedA = normalizeConditionKeys(a);
|
|
830
|
+
const normalizedB = normalizeConditionKeys(b);
|
|
831
|
+
// Collect all unique operators from both sides
|
|
832
|
+
const allOperators = Array.from(new Set([...Object.keys(normalizedA), ...Object.keys(normalizedB)]));
|
|
833
|
+
const result = {};
|
|
834
|
+
for (const operator of allOperators) {
|
|
835
|
+
const keysA = normalizedA[operator] || {};
|
|
836
|
+
const keysB = normalizedB[operator] || {};
|
|
837
|
+
// Collect all unique context keys for this operator
|
|
838
|
+
const allContextKeys = Array.from(new Set([...Object.keys(keysA), ...Object.keys(keysB)]));
|
|
839
|
+
result[operator] = {};
|
|
840
|
+
for (const contextKey of allContextKeys) {
|
|
841
|
+
const valsA = keysA[contextKey];
|
|
842
|
+
const valsB = keysB[contextKey];
|
|
843
|
+
// If key exists in both sides, apply intersection logic based on operator type
|
|
844
|
+
if (valsA !== undefined && valsB !== undefined) {
|
|
845
|
+
const intersectedValues = intersectValuesForOperator(operator, valsA, valsB);
|
|
846
|
+
if (intersectedValues === null) {
|
|
847
|
+
// Empty intersection means no overlap - return null
|
|
848
|
+
return null;
|
|
849
|
+
}
|
|
850
|
+
result[operator][contextKey] = intersectedValues;
|
|
851
|
+
}
|
|
852
|
+
else {
|
|
853
|
+
// Key only exists in one side - carry it through (both conditions must be satisfied)
|
|
854
|
+
result[operator][contextKey] = valsA !== undefined ? Array.from(valsA) : Array.from(valsB);
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
// Remove empty operator objects
|
|
858
|
+
if (Object.keys(result[operator]).length === 0) {
|
|
859
|
+
delete result[operator];
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
const merged = mergeComplementaryConditions(result);
|
|
863
|
+
// Check if any values array became empty after merging complementary conditions
|
|
864
|
+
// (e.g., StringEquals: ['a'] merged with StringNotEquals: ['a'] results in StringEquals: [])
|
|
865
|
+
for (const [, keyMap] of Object.entries(merged)) {
|
|
866
|
+
for (const [, values] of Object.entries(keyMap)) {
|
|
867
|
+
if (values.length === 0) {
|
|
868
|
+
return null;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return merged;
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Intersect values for a specific operator type.
|
|
876
|
+
*
|
|
877
|
+
* Returns the intersected values, or null if the intersection is empty
|
|
878
|
+
* (meaning the conditions are mutually exclusive).
|
|
879
|
+
*/
|
|
880
|
+
function intersectValuesForOperator(operator, valsA, valsB) {
|
|
881
|
+
const baseOp = conditionBaseOperator(operator);
|
|
882
|
+
switch (baseOp) {
|
|
883
|
+
// String/ARN equality operators: intersection of allowed values
|
|
884
|
+
case 'stringequals':
|
|
885
|
+
case 'stringlike':
|
|
886
|
+
case 'arnequals':
|
|
887
|
+
case 'arnlike': {
|
|
888
|
+
const common = valsA.filter((v) => valsB.includes(v));
|
|
889
|
+
return common.length > 0 ? common : null;
|
|
890
|
+
}
|
|
891
|
+
// String/ARN negation operators: union of exclusions (more restrictive)
|
|
892
|
+
case 'stringnotequals':
|
|
893
|
+
case 'stringnotlike':
|
|
894
|
+
case 'arnnotequals':
|
|
895
|
+
case 'arnnotlike': {
|
|
896
|
+
return Array.from(new Set([...valsA, ...valsB]));
|
|
897
|
+
}
|
|
898
|
+
// Numeric less-than operators: take the minimum (more restrictive)
|
|
899
|
+
case 'numericlessthan':
|
|
900
|
+
case 'numericlessthanequals': {
|
|
901
|
+
const numA = Number(valsA[0]);
|
|
902
|
+
const numB = Number(valsB[0]);
|
|
903
|
+
if (isNaN(numA) || isNaN(numB)) {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
return [String(Math.min(numA, numB))];
|
|
907
|
+
}
|
|
908
|
+
// Numeric greater-than operators: take the maximum (more restrictive)
|
|
909
|
+
case 'numericgreaterthan':
|
|
910
|
+
case 'numericgreaterthanequals': {
|
|
911
|
+
const numA = Number(valsA[0]);
|
|
912
|
+
const numB = Number(valsB[0]);
|
|
913
|
+
if (isNaN(numA) || isNaN(numB)) {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
return [String(Math.max(numA, numB))];
|
|
917
|
+
}
|
|
918
|
+
// Boolean operators: values must match exactly
|
|
919
|
+
case 'bool':
|
|
920
|
+
case 'null': {
|
|
921
|
+
if (valsA[0]?.toLowerCase() !== valsB[0]?.toLowerCase()) {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
return [valsA[0]];
|
|
925
|
+
}
|
|
926
|
+
// IP address operators: intersection of CIDR blocks
|
|
927
|
+
case 'ipaddress':
|
|
928
|
+
case 'notipaddress': {
|
|
929
|
+
const common = valsA.filter((cidr) => valsB.includes(cidr));
|
|
930
|
+
return common.length > 0 ? common : null;
|
|
931
|
+
}
|
|
932
|
+
// Date less-than operators: take the earlier date (more restrictive)
|
|
933
|
+
case 'datelessthan':
|
|
934
|
+
case 'datelessthanequals': {
|
|
935
|
+
const dateA = valsA[0];
|
|
936
|
+
const dateB = valsB[0];
|
|
937
|
+
return [dateA < dateB ? dateA : dateB];
|
|
938
|
+
}
|
|
939
|
+
// Date greater-than operators: take the later date (more restrictive)
|
|
940
|
+
case 'dategreaterthan':
|
|
941
|
+
case 'dategreaterthanequals': {
|
|
942
|
+
const dateA = valsA[0];
|
|
943
|
+
const dateB = valsB[0];
|
|
944
|
+
return [dateA > dateB ? dateA : dateB];
|
|
945
|
+
}
|
|
946
|
+
// Unknown operator - cannot handle
|
|
947
|
+
default:
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
}
|
|
897
951
|
/**
|
|
898
952
|
* Checks if an IAM condition operator ends with "IfExists".
|
|
899
953
|
*
|
|
@@ -1016,10 +1070,10 @@ function mergeComplementaryConditions(c) {
|
|
|
1016
1070
|
datelessthanequals: 'dategreaterthan',
|
|
1017
1071
|
dategreaterthan: 'datelessthanequals',
|
|
1018
1072
|
ipaddress: 'notipaddress',
|
|
1019
|
-
notipaddress: 'ipaddress'
|
|
1020
|
-
bool: 'bool'
|
|
1073
|
+
notipaddress: 'ipaddress'
|
|
1074
|
+
// bool: 'bool'
|
|
1021
1075
|
};
|
|
1022
|
-
const out =
|
|
1076
|
+
const out = structuredClone(c);
|
|
1023
1077
|
for (const [base, comp] of Object.entries(complement)) {
|
|
1024
1078
|
if (out[base] && out[comp]) {
|
|
1025
1079
|
for (const key of Object.keys(out[base])) {
|
|
@@ -1034,4 +1088,43 @@ function mergeComplementaryConditions(c) {
|
|
|
1034
1088
|
}
|
|
1035
1089
|
return out;
|
|
1036
1090
|
}
|
|
1091
|
+
/**
|
|
1092
|
+
* Apply Deny conditions to an Allow permission.
|
|
1093
|
+
*
|
|
1094
|
+
* A Deny permission with conditions (whether multiple operators or multiple keys under one
|
|
1095
|
+
* operator) acts as an AND, meaning the Allow needs to escape ANY one of them (OR when inverted).
|
|
1096
|
+
* Each condition key-value pair is inverted and creates a separate Allow permission.
|
|
1097
|
+
*
|
|
1098
|
+
* It is possible for any given condition to fully deny the Allow, in which case
|
|
1099
|
+
* that condition will produce no resulting Allow permission. The result is an array
|
|
1100
|
+
* of Allow permissions that apply after each Deny condition is applied.
|
|
1101
|
+
*
|
|
1102
|
+
* This may result in multiple Allow permission or an empty array if all are denied.
|
|
1103
|
+
*
|
|
1104
|
+
* @param allow the Allow permission
|
|
1105
|
+
* @param deny the Deny permission
|
|
1106
|
+
* @returns an array of resulting Allow permissions after applying Deny conditions
|
|
1107
|
+
*/
|
|
1108
|
+
export function applyDenyConditionsToAllow(allow, deny) {
|
|
1109
|
+
// If Deny has no conditions, it fully denies the Allow
|
|
1110
|
+
if (!deny.conditions || Object.keys(deny.conditions).length === 0) {
|
|
1111
|
+
return [allow];
|
|
1112
|
+
}
|
|
1113
|
+
const results = [];
|
|
1114
|
+
// Each Deny condition key-value pair creates a separate inverted condition for Allow
|
|
1115
|
+
// (multiple keys under the same operator are each inverted separately)
|
|
1116
|
+
for (const [operator, keyMap] of Object.entries(deny.conditions || {})) {
|
|
1117
|
+
for (const [contextKey, values] of Object.entries(keyMap)) {
|
|
1118
|
+
// Invert this specific condition
|
|
1119
|
+
const singleCondition = { [operator]: { [contextKey]: values } };
|
|
1120
|
+
const invertedCondition = invertConditions(singleCondition);
|
|
1121
|
+
// Merge with the original Allow conditions
|
|
1122
|
+
const mergedConditions = intersectConditions(allow.conditions || {}, invertedCondition);
|
|
1123
|
+
if (mergedConditions !== null) {
|
|
1124
|
+
results.push(new Permission(allow.effect, allow.service, allow.action, allow.resource, allow.notResource, mergedConditions));
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
return results;
|
|
1129
|
+
}
|
|
1037
1130
|
//# sourceMappingURL=permission.js.map
|