@actsecurity/iam-lens 0.1.165 → 0.1.167

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.
Files changed (33) hide show
  1. package/dist/cjs/index.d.ts +1 -1
  2. package/dist/cjs/index.d.ts.map +1 -1
  3. package/dist/cjs/index.js +2 -1
  4. package/dist/cjs/index.js.map +1 -1
  5. package/dist/cjs/simulate/contextKeys.d.ts +4 -0
  6. package/dist/cjs/simulate/contextKeys.d.ts.map +1 -1
  7. package/dist/cjs/simulate/contextKeys.js +5 -3
  8. package/dist/cjs/simulate/contextKeys.js.map +1 -1
  9. package/dist/cjs/simulate/roleSessionTags.d.ts +51 -0
  10. package/dist/cjs/simulate/roleSessionTags.d.ts.map +1 -0
  11. package/dist/cjs/simulate/roleSessionTags.js +218 -0
  12. package/dist/cjs/simulate/roleSessionTags.js.map +1 -0
  13. package/dist/cjs/simulate/simulate.d.ts +35 -2
  14. package/dist/cjs/simulate/simulate.d.ts.map +1 -1
  15. package/dist/cjs/simulate/simulate.js +163 -11
  16. package/dist/cjs/simulate/simulate.js.map +1 -1
  17. package/dist/esm/index.d.ts +1 -1
  18. package/dist/esm/index.d.ts.map +1 -1
  19. package/dist/esm/index.js +1 -1
  20. package/dist/esm/index.js.map +1 -1
  21. package/dist/esm/simulate/contextKeys.d.ts +4 -0
  22. package/dist/esm/simulate/contextKeys.d.ts.map +1 -1
  23. package/dist/esm/simulate/contextKeys.js +5 -3
  24. package/dist/esm/simulate/contextKeys.js.map +1 -1
  25. package/dist/esm/simulate/roleSessionTags.d.ts +51 -0
  26. package/dist/esm/simulate/roleSessionTags.d.ts.map +1 -0
  27. package/dist/esm/simulate/roleSessionTags.js +213 -0
  28. package/dist/esm/simulate/roleSessionTags.js.map +1 -0
  29. package/dist/esm/simulate/simulate.d.ts +35 -2
  30. package/dist/esm/simulate/simulate.d.ts.map +1 -1
  31. package/dist/esm/simulate/simulate.js +163 -12
  32. package/dist/esm/simulate/simulate.js.map +1 -1
  33. package/package.json +1 -1
@@ -0,0 +1,213 @@
1
+ import { loadPolicy } from '@actsecurity/iam-policy';
2
+ import { actionMatchesPattern } from '@actsecurity/iam-utils';
3
+ const tagSessionAction = 'sts:TagSession';
4
+ const tagKeysConditionKey = 'aws:tagkeys';
5
+ const policyVariablePattern = /\$\{[^}]+\}/;
6
+ /** No trust-policy Allow permits session tags. */
7
+ const noSessionTags = { type: 'none' };
8
+ /** At least one trust-policy Allow may permit any session-tag key. */
9
+ const anySessionTag = { type: 'any' };
10
+ /**
11
+ * Determines which tag keys a role session may supply from the role's trust policy.
12
+ *
13
+ * This result intentionally does not depend on tags currently stored on the role. Deny statements
14
+ * and conditions unrelated to `aws:TagKeys` are ignored. Supported
15
+ * `ForAllValues:StringEquals` operators form a finite allowlist for one Allow statement, while
16
+ * `ForAnyValue`, unsupported operators, and policy-variable values conservatively permit any key.
17
+ *
18
+ * @param trustPolicy the role's validated trust policy, or undefined when it was not collected
19
+ * @returns the role's session-tag key capability
20
+ */
21
+ export function roleSessionTagCapability(trustPolicy) {
22
+ if (!trustPolicy) {
23
+ return noSessionTags;
24
+ }
25
+ if (trustPolicy.errors.length > 0) {
26
+ return anySessionTag;
27
+ }
28
+ const allowedTagKeys = [];
29
+ let hasTagSessionAllow = false;
30
+ for (const statement of loadPolicy(trustPolicy).statements()) {
31
+ if (!statement.isAllow() || !statementAllowsTagSession(statement)) {
32
+ continue;
33
+ }
34
+ hasTagSessionAllow = true;
35
+ const statementCapability = tagCapabilityForStatement(statement);
36
+ if (statementCapability.type === 'any') {
37
+ return anySessionTag;
38
+ }
39
+ if (statementCapability.type === 'allowlist') {
40
+ allowedTagKeys.push(...statementCapability.tagKeys);
41
+ }
42
+ }
43
+ if (!hasTagSessionAllow) {
44
+ return noSessionTags;
45
+ }
46
+ return {
47
+ type: 'allowlist',
48
+ tagKeys: uniqueTagKeys(allowedTagKeys)
49
+ };
50
+ }
51
+ /**
52
+ * Determines whether a tag key can be supplied by a role session.
53
+ *
54
+ * @param capability the trust-policy-derived session-tag capability
55
+ * @param tagKey the tag key to test
56
+ * @returns true when a role session may supply the key
57
+ */
58
+ export function roleSessionCanSetTagKey(capability, tagKey) {
59
+ if (capability.type === 'any') {
60
+ return true;
61
+ }
62
+ if (capability.type === 'none') {
63
+ return false;
64
+ }
65
+ return capability.tagKeys.some((entry) => tagKeyMatchesEntry(tagKey, entry));
66
+ }
67
+ /**
68
+ * Builds a slash-delimited regex constraint for PrincipalTag keys that cannot be session supplied.
69
+ *
70
+ * The iam-simulate constraint matcher is case-insensitive. Consequently, case variants of a
71
+ * case-sensitive allowlist entry are also excluded from this strict pattern. That may preserve
72
+ * extra conditional results, but it cannot incorrectly make a mutable tag strict.
73
+ *
74
+ * @param capability the trust-policy-derived session-tag capability
75
+ * @returns a strict PrincipalTag pattern, or undefined when every tag key may be supplied
76
+ */
77
+ export function immutablePrincipalTagPattern(capability) {
78
+ if (capability.type === 'any') {
79
+ return undefined;
80
+ }
81
+ if (capability.type === 'none' || capability.tagKeys.length === 0) {
82
+ return '/^aws:PrincipalTag\/.*/';
83
+ }
84
+ const mutableNames = capability.tagKeys.map((entry) => escapeRegex(entry.tagKey)).join('|');
85
+ return `/^aws:PrincipalTag\\/(?!(?:${mutableNames})$).*/`;
86
+ }
87
+ /**
88
+ * Determines whether an Allow statement's Action or NotAction includes `sts:TagSession`.
89
+ *
90
+ * @param statement the parsed trust-policy statement to inspect
91
+ * @returns true when the statement applies to the TagSession action
92
+ */
93
+ function statementAllowsTagSession(statement) {
94
+ if (statement.isActionStatement()) {
95
+ return statement
96
+ .actions()
97
+ .some((action) => actionMatchesPattern(tagSessionAction, action.value()));
98
+ }
99
+ if (statement.isNotActionStatement()) {
100
+ return !statement
101
+ .notActions()
102
+ .some((action) => actionMatchesPattern(tagSessionAction, action.value()));
103
+ }
104
+ return false;
105
+ }
106
+ /**
107
+ * Determines which tag keys one TagSession Allow statement may supply.
108
+ *
109
+ * @param statement a parsed Allow statement that includes TagSession
110
+ * @returns the statement's session-tag capability
111
+ */
112
+ function tagCapabilityForStatement(statement) {
113
+ const tagKeysConditions = statement
114
+ .conditions()
115
+ .filter((condition) => stringsEqualIgnoreCase(condition.conditionKey(), tagKeysConditionKey));
116
+ if (tagKeysConditions.length === 0 || tagKeysConditions.some(conditionIsUnbounded)) {
117
+ return anySessionTag;
118
+ }
119
+ return {
120
+ type: 'allowlist',
121
+ tagKeys: intersectTagKeyConditions(tagKeysConditions)
122
+ };
123
+ }
124
+ /**
125
+ * Determines whether a TagKeys condition cannot prove a finite exhaustive key allowlist.
126
+ *
127
+ * @param condition the parsed `aws:TagKeys` condition
128
+ * @returns true when the statement must conservatively be treated as permitting any key
129
+ */
130
+ function conditionIsUnbounded(condition) {
131
+ const operation = condition.operation();
132
+ if (operation.isIfExists() || operation.setOperator() !== 'ForAllValues') {
133
+ return true;
134
+ }
135
+ const baseOperator = operation.baseOperator();
136
+ if (!stringsEqualIgnoreCase(baseOperator, 'StringEquals') &&
137
+ !stringsEqualIgnoreCase(baseOperator, 'StringEqualsIgnoreCase')) {
138
+ return true;
139
+ }
140
+ return condition.conditionValues().some((value) => policyVariablePattern.test(value));
141
+ }
142
+ /**
143
+ * Intersects finite ForAllValues TagKeys allowlists within one statement.
144
+ *
145
+ * @param conditions supported TagKeys conditions from one statement
146
+ * @returns tag keys capable of satisfying every condition
147
+ */
148
+ function intersectTagKeyConditions(conditions) {
149
+ const conditionEntries = conditions.map(entriesForCondition);
150
+ const hasCaseSensitiveCondition = conditionEntries.some((entries) => entries.some((entry) => !entry.ignoreCase));
151
+ const candidates = hasCaseSensitiveCondition
152
+ ? conditionEntries.flat().filter((entry) => !entry.ignoreCase)
153
+ : conditionEntries[0];
154
+ return uniqueTagKeys(candidates.filter((candidate) => conditionEntries.every((entries) => entries.some((entry) => tagKeyMatchesEntry(candidate.tagKey, entry)))));
155
+ }
156
+ /**
157
+ * Converts one supported TagKeys condition into comparable entries.
158
+ *
159
+ * @param condition the parsed supported condition
160
+ * @returns entries carrying the condition's case-comparison semantics
161
+ */
162
+ function entriesForCondition(condition) {
163
+ const ignoreCase = stringsEqualIgnoreCase(condition.operation().baseOperator(), 'StringEqualsIgnoreCase');
164
+ return condition.conditionValues().map((tagKey) => ({ tagKey, ignoreCase }));
165
+ }
166
+ /**
167
+ * Tests a concrete tag key against a trust-policy allowlist entry.
168
+ *
169
+ * @param tagKey the concrete tag key
170
+ * @param entry the allowlist entry and its comparison semantics
171
+ * @returns true when the key matches the entry
172
+ */
173
+ function tagKeyMatchesEntry(tagKey, entry) {
174
+ return entry.ignoreCase ? stringsEqualIgnoreCase(tagKey, entry.tagKey) : tagKey === entry.tagKey;
175
+ }
176
+ /**
177
+ * Removes semantically duplicate tag-key entries while preserving case-sensitive alternatives.
178
+ *
179
+ * @param entries tag-key entries to deduplicate
180
+ * @returns unique entries
181
+ */
182
+ function uniqueTagKeys(entries) {
183
+ const unique = [];
184
+ for (const entry of entries) {
185
+ if (!unique.some((existing) => existing.ignoreCase === entry.ignoreCase &&
186
+ (entry.ignoreCase
187
+ ? stringsEqualIgnoreCase(existing.tagKey, entry.tagKey)
188
+ : existing.tagKey === entry.tagKey))) {
189
+ unique.push(entry);
190
+ }
191
+ }
192
+ return unique;
193
+ }
194
+ /**
195
+ * Compares strings without case sensitivity or normalized string allocations.
196
+ *
197
+ * @param left first value to compare
198
+ * @param right second value to compare
199
+ * @returns true when the values compare equally without case sensitivity
200
+ */
201
+ function stringsEqualIgnoreCase(left, right) {
202
+ return left.localeCompare(right, undefined, { sensitivity: 'base' }) === 0;
203
+ }
204
+ /**
205
+ * Escapes a literal string for use inside a regular expression.
206
+ *
207
+ * @param value the literal value to escape
208
+ * @returns a regex-safe representation
209
+ */
210
+ function escapeRegex(value) {
211
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
212
+ }
213
+ //# sourceMappingURL=roleSessionTags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roleSessionTags.js","sourceRoot":"","sources":["../../../src/simulate/roleSessionTags.ts"],"names":[],"mappings":"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"}
@@ -54,6 +54,18 @@ export interface AnonymousSimulationRequest extends Omit<SimulationRequest, 'pri
54
54
  */
55
55
  resourceArn: string;
56
56
  }
57
+ /**
58
+ * A simulation request for a principal ARN that is not collected as an IAM identity.
59
+ *
60
+ * The principal's captured organization policy hierarchies still apply. Its identity policies
61
+ * and permission boundary are replaced by one synthetic allow-all policy or `inlinePolicy`.
62
+ */
63
+ export interface VirtualPrincipalSimulationRequest extends SimulationRequest {
64
+ /**
65
+ * Raw inline identity policy that replaces the default synthetic allow-all policy.
66
+ */
67
+ inlinePolicy?: Record<string, unknown>;
68
+ }
57
69
  /**
58
70
  * A simulation request targeting a resource that is not expected to exist in the collected dataset.
59
71
  */
@@ -89,6 +101,10 @@ export type InternalSimulationRequest = Omit<SimulationRequest, 'principal'> & {
89
101
  * Authenticated principal string, or undefined for an anonymous request.
90
102
  */
91
103
  principal: string | undefined;
104
+ /**
105
+ * Caller-supplied inline identity policy for a virtual principal.
106
+ */
107
+ inlinePolicy?: Record<string, unknown>;
92
108
  /**
93
109
  * Caller-supplied external resource or trust policy.
94
110
  */
@@ -147,6 +163,18 @@ export declare function simulateAnonymousRequest(simulationRequest: AnonymousSim
147
163
  * @returns the normalized request and simulation result
148
164
  */
149
165
  export declare function simulateExternalPrincipalRequest(simulationRequest: SimulationRequest, collectClient: IamCollectClient): Promise<SimulateRequestResult>;
166
+ /**
167
+ * Simulate an uncollected IAM principal while applying organization controls for its account.
168
+ *
169
+ * The principal receives a synthetic allow-all identity policy unless `inlinePolicy` replaces it.
170
+ * Permission boundaries and collected identity policies do not apply; supplied session policies still
171
+ * constrain the request.
172
+ *
173
+ * @param simulationRequest the virtual principal request details
174
+ * @param collectClient the IAM collect client to use for organization and resource-side data access
175
+ * @returns the normalized request and simulation result
176
+ */
177
+ export declare function simulateVirtualPrincipal(simulationRequest: VirtualPrincipalSimulationRequest, collectClient: IamCollectClient): Promise<SimulateRequestResult>;
150
178
  /**
151
179
  * Simulate a collected principal accessing a resource outside the collected dataset.
152
180
  * Wildcard-only actions are unsupported.
@@ -157,14 +185,19 @@ export declare function simulateExternalPrincipalRequest(simulationRequest: Simu
157
185
  */
158
186
  export declare function simulateExternalResourceRequest(simulationRequest: ExternalResourceSimulationRequest, collectClient: IamCollectClient): Promise<SimulateRequestResult>;
159
187
  /**
160
- * Convert an iam-lens strict context key into an iam-simulate Discovery constraint.
188
+ * Convert an iam-lens context key into an iam-simulate Discovery constraint.
189
+ *
190
+ * Caller-provided and explicitly strict keys remain fully authoritative. A stored PrincipalTag
191
+ * whose role session may override it has known presence but an unknown value. All other input keys
192
+ * are strict and continue through the existing special-case handling.
161
193
  *
162
194
  * @param keyName the literal context key or slash-delimited key pattern
163
195
  * @param simulationRequest the normalized request whose context certainty is modeled
196
+ * @param mutablePrincipalTagKeys stored PrincipalTag keys whose values are non-authoritative
164
197
  * @returns the Discovery constraint to pass to iam-simulate
165
198
  * @internal
166
199
  */
167
- export declare function discoveryConstraintForStrictKey(keyName: string, simulationRequest: Pick<InternalSimulationRequest, 'customContextKeys' | 'simulationMode' | 'principal'>): DiscoveryContextKeyConstraint;
200
+ export declare function discoveryConstraintForStrictKey(keyName: string, simulationRequest: Pick<InternalSimulationRequest, 'additionalStrictContextKeys' | 'customContextKeys' | 'simulationMode' | 'principal'>, mutablePrincipalTagKeys?: ReadonlySet<string>): DiscoveryContextKeyConstraint;
168
201
  /**
169
202
  * Compare an actual simulation decision with an optional expected result.
170
203
  *
@@ -1 +1 @@
1
- {"version":3,"file":"simulate.d.ts","sourceRoot":"","sources":["../../../src/simulate/simulate.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"simulate.d.ts","sourceRoot":"","sources":["../../../src/simulate/simulate.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,6BAA6B,EAClC,KAAK,gBAAgB,EACrB,KAAK,oBAAoB,EAEzB,KAAK,UAAU,EACf,KAAK,cAAc,EACpB,MAAM,2BAA2B,CAAA;AASlC,OAAO,EAAE,gBAAgB,EAAE,KAAK,qBAAqB,EAAE,MAAM,sBAAsB,CAAA;AAanF,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,OAAO,EAEL,KAAK,WAAW,EAKjB,MAAM,kBAAkB,CAAA;AAWzB;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,WAAW,EAAE,MAAM,GAAG,SAAS,CAAA;IAE/B;;OAEG;IACH,eAAe,EAAE,MAAM,GAAG,SAAS,CAAA;IAEnC;;OAEG;IACH,MAAM,EAAE,MAAM,CAAA;IAEd;;OAEG;IACH,SAAS,EAAE,MAAM,CAAA;IAEjB;;;OAGG;IACH,iBAAiB,EAAE,WAAW,CAAA;IAE9B;;OAEG;IACH,cAAc,EAAE,cAAc,CAAA;IAE9B;;OAEG;IACH,cAAc,CAAC,EAAE,cAAc,CAAA;IAE/B;;OAEG;IACH,aAAa,CAAC,EAAE,GAAG,CAAA;IAEnB;;OAEG;IACH,2BAA2B,CAAC,EAAE,MAAM,EAAE,CAAA;CACvC;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA2B,SAAQ,IAAI,CACtD,iBAAiB,EACjB,WAAW,GAAG,aAAa,GAAG,eAAe,CAC9C;IACC;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;CACpB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iCAAkC,SAAQ,iBAAiB;IAC1E;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACvC;AAED;;GAEG;AACH,MAAM,WAAW,iCAAkC,SAAQ,IAAI,CAAC,iBAAiB,EAAE,aAAa,CAAC;IAC/F;;OAEG;IACH,WAAW,EAAE,MAAM,CAAA;IAEnB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,OAAO,EAAE,UAAU,CAAC,SAAS,CAAC,CAAA;IAE9B;;OAEG;IACH,MAAM,EAAE,oBAAoB,CAAA;CAC7B;AAQD;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG,IAAI,CAAC,iBAAiB,EAAE,WAAW,CAAC,GAAG;IAC7E;;OAEG;IACH,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IAE7B;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAEtC;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CACzC,CAAA;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC;;OAEG;IACH,gBAAgB,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,EAAE,CAAA;IAEjD;;OAEG;IACH,sBAAsB,EAAE,qBAAqB,EAAE,CAAA;IAE/C;;OAEG;IACH,0BAA0B,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,GAAG,CAAA;KAAE,EAAE,GAAG,SAAS,CAAA;IAEvE;;OAEG;IACH,oBAAoB,EAAE,qBAAqB,EAAE,CAAA;CAC9C;AAiBD;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,iBAAiB,EAAE,iBAAiB,EACpC,aAAa,EAAE,gBAAgB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,iBAAiB,EAAE,0BAA0B,EAC7C,aAAa,EAAE,gBAAgB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAMhC;AAED;;;;;;;;GAQG;AACH,wBAAsB,gCAAgC,CACpD,iBAAiB,EAAE,iBAAiB,EACpC,aAAa,EAAE,gBAAgB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,wBAAwB,CAC5C,iBAAiB,EAAE,iCAAiC,EACpD,aAAa,EAAE,gBAAgB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAED;;;;;;;GAOG;AACH,wBAAsB,+BAA+B,CACnD,iBAAiB,EAAE,iCAAiC,EACpD,aAAa,EAAE,gBAAgB,GAC9B,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAyiBD;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,MAAM,EACf,iBAAiB,EAAE,IAAI,CACrB,yBAAyB,EACzB,6BAA6B,GAAG,mBAAmB,GAAG,gBAAgB,GAAG,WAAW,CACrF,EACD,uBAAuB,CAAC,EAAE,WAAW,CAAC,MAAM,CAAC,GAC5C,6BAA6B,CAoC/B;AAoLD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,gBAAgB,GAAG,SAAS,GAAG,SAAS,EAClD,MAAM,EAAE,gBAAgB,GACvB,OAAO,CAQT"}
@@ -1,7 +1,7 @@
1
1
  import { iamActionDetails, iamActionExists, iamServiceExists } from '@actsecurity/iam-data';
2
2
  import {} from '@actsecurity/iam-policy';
3
3
  import { anonymousPrincipal, runSimulation } from '@actsecurity/iam-simulate';
4
- import { convertAssumedRoleArnToRoleArn, isAssumedRoleArn, isIamRoleArn, isS3BucketOrObjectArn, splitArnParts } from '@actsecurity/iam-utils';
4
+ import { convertAssumedRoleArnToRoleArn, isAssumedRoleArn, isIamRoleArn, isIamUserArn, isS3BucketOrObjectArn, splitArnParts } from '@actsecurity/iam-utils';
5
5
  import { IamCollectClient } from '../collect/client.js';
6
6
  import { getAllPoliciesForPrincipal, isServiceLinkedRole, isServicePrincipal, principalExists } from '../principals.js';
7
7
  import { getAccountIdForResource, getRcpsForResource, getResourcePolicyForResource } from '../resources.js';
@@ -9,6 +9,7 @@ import {} from '../utils/s3Abac.js';
9
9
  import { AssumeRoleActions } from '../utils/sts.js';
10
10
  import { CONTEXT_KEYS, contextValue, createContextKeys, isAnonymousPrincipalContextKey, knownContextKeys } from './contextKeys.js';
11
11
  import { externalResourcePolicy, syntheticIdentityPolicy } from './generatedPolicies.js';
12
+ import { immutablePrincipalTagPattern, roleSessionCanSetTagKey, roleSessionTagCapability } from './roleSessionTags.js';
12
13
  const kmsRetireGrantAction = 'kms:retiregrant';
13
14
  /**
14
15
  * Simulate an IAM request using the collected policies for both the principal and resource.
@@ -42,6 +43,20 @@ export async function simulateAnonymousRequest(simulationRequest, collectClient)
42
43
  export async function simulateExternalPrincipalRequest(simulationRequest, collectClient) {
43
44
  return simulateRequestInternal({ ...simulationRequest }, 'externalPrincipal', collectClient);
44
45
  }
46
+ /**
47
+ * Simulate an uncollected IAM principal while applying organization controls for its account.
48
+ *
49
+ * The principal receives a synthetic allow-all identity policy unless `inlinePolicy` replaces it.
50
+ * Permission boundaries and collected identity policies do not apply; supplied session policies still
51
+ * constrain the request.
52
+ *
53
+ * @param simulationRequest the virtual principal request details
54
+ * @param collectClient the IAM collect client to use for organization and resource-side data access
55
+ * @returns the normalized request and simulation result
56
+ */
57
+ export async function simulateVirtualPrincipal(simulationRequest, collectClient) {
58
+ return simulateRequestInternal({ ...simulationRequest }, 'virtualPrincipal', collectClient);
59
+ }
45
60
  /**
46
61
  * Simulate a collected principal accessing a resource outside the collected dataset.
47
62
  * Wildcard-only actions are unsupported.
@@ -76,7 +91,11 @@ async function simulateRequestInternal(input, variant, collectClient) {
76
91
  throw new Error(`Unable to find action details for ${simulationRequest.action}`);
77
92
  }
78
93
  const actionDetails = await iamActionDetails(service, serviceAction);
79
- if (actionDetails.isWildcardOnly && variant !== 'normal') {
94
+ if (variant === 'virtualPrincipal' && !splitArnParts(simulationRequest.principal).accountId) {
95
+ throw new Error(`Virtual principal ${simulationRequest.principal} must contain an account ID.`);
96
+ }
97
+ if (actionDetails.isWildcardOnly &&
98
+ (variant === 'anonymous' || variant === 'externalPrincipal' || variant === 'externalResource')) {
80
99
  const variantName = variant === 'anonymous'
81
100
  ? 'Anonymous'
82
101
  : variant === 'externalPrincipal'
@@ -124,7 +143,7 @@ async function simulateRequestInternal(input, variant, collectClient) {
124
143
  ? await resolvePrincipalArnForContext(collectClient, simulationRequest.principal)
125
144
  : undefined;
126
145
  const includeResourceMetadata = variant !== 'externalResource';
127
- const { contextKeys, resourceTagsAreKnown } = await createContextKeys(collectClient, simulationRequest, service, simulationRequest.customContextKeys, principalArnForContext, { includeResourceMetadata });
146
+ const { contextKeys, resourceTagsAreKnown } = await createContextKeys(collectClient, simulationRequest, service, simulationRequest.customContextKeys, principalArnForContext, { includePrincipalTags: requiresCollectedPrincipal, includeResourceMetadata });
128
147
  const vpcEndpointPolicy = await getVpcEndpointPolicy(collectClient, contextKeys);
129
148
  request.contextVariables = contextKeys;
130
149
  const simulation = {
@@ -138,8 +157,15 @@ async function simulateRequestInternal(input, variant, collectClient) {
138
157
  vpcEndpointPolicies: vpcEndpointPolicy ? [vpcEndpointPolicy] : undefined
139
158
  };
140
159
  await applyS3Settings(simulation, simulationRequest, variant, collectClient);
141
- const strictContextKeys = makeStrictContextKeys(simulationRequest, contextKeys, resourceTagsAreKnown, isS3BucketOrObjectArn(simulationRequest.resourceArn || ''));
142
- const discoveryContextKeyConstraints = strictContextKeys.map((keyName) => discoveryConstraintForStrictKey(keyName, simulationRequest));
160
+ const sessionTagCapability = await sessionTagCapabilityForRequest(collectClient, simulationRequest, principalArnForContext, requiresCollectedPrincipal);
161
+ const mutableExistingPrincipalTagKeys = mutableStoredPrincipalTagKeys(sessionTagCapability, contextKeys, simulationRequest.customContextKeys);
162
+ const principalTagsAreUnknown = variant === 'externalPrincipal' || variant === 'virtualPrincipal';
163
+ const strictContextKeys = makeStrictContextKeys(simulationRequest, contextKeys, resourceTagsAreKnown, isS3BucketOrObjectArn(simulationRequest.resourceArn || ''), principalTagsAreUnknown, sessionTagCapability, mutableExistingPrincipalTagKeys);
164
+ const discoveryContextKeyNames = new Set([
165
+ ...strictContextKeys,
166
+ ...mutableExistingPrincipalTagKeys
167
+ ]);
168
+ const discoveryContextKeyConstraints = [...discoveryContextKeyNames].map((keyName) => discoveryConstraintForStrictKey(keyName, simulationRequest, mutableExistingPrincipalTagKeys));
143
169
  const result = await runSimulation(simulation, {
144
170
  simulationMode: simulationRequest.simulationMode,
145
171
  discoveryContextKeyConstraints
@@ -150,8 +176,9 @@ async function simulateRequestInternal(input, variant, collectClient) {
150
176
  * Build all principal-side policy inputs for the selected simulation variant.
151
177
  *
152
178
  * Collected-principal variants prepare collected identity policies, SCPs, and permission
153
- * boundaries. External principals receive only a synthetic administrator identity policy, while
154
- * anonymous requests receive no principal-side policies.
179
+ * boundaries. External principals receive only a synthetic administrator identity policy, virtual
180
+ * principals receive a synthetic or caller-provided identity policy plus captured organization
181
+ * policies, and anonymous requests receive no principal-side policies.
155
182
  *
156
183
  * @param variant the simulation policy-source variant
157
184
  * @param request the normalized request
@@ -169,6 +196,26 @@ async function principalPoliciesForVariant(variant, request, collectClient) {
169
196
  };
170
197
  }
171
198
  const principalArn = request.principal;
199
+ if (variant === 'virtualPrincipal') {
200
+ const accountId = splitArnParts(principalArn).accountId;
201
+ if (!accountId) {
202
+ throw new Error(`Virtual principal ${principalArn} must contain an account ID.`);
203
+ }
204
+ const [serviceControlPolicies, principalAccountRcps] = await Promise.all([
205
+ collectClient.getScpHierarchyForAccount(accountId),
206
+ collectClient.getRcpHierarchyForAccount(accountId)
207
+ ]);
208
+ return {
209
+ identityPolicies: [
210
+ request.inlinePolicy
211
+ ? { name: 'iam-lens:virtual-inline-policy', policy: request.inlinePolicy }
212
+ : syntheticIdentityPolicy
213
+ ],
214
+ serviceControlPolicies,
215
+ permissionBoundaryPolicies: undefined,
216
+ principalAccountRcps
217
+ };
218
+ }
172
219
  const collectedPolicies = await getAllPoliciesForPrincipal(collectClient, principalArn);
173
220
  return {
174
221
  identityPolicies: prepareIdentityPolicies(principalArn, collectedPolicies),
@@ -266,9 +313,12 @@ async function applyS3Settings(simulation, request, variant, collectClient) {
266
313
  * @param contextKeys generated and caller-provided context values
267
314
  * @param resourceTagsAreKnown whether all resource tags are known
268
315
  * @param s3BucketOrObjectRequest whether S3 bucket tag keys apply
316
+ * @param principalTagsAreUnknown whether synthetic principal tag presence and values are unknown
317
+ * @param sessionTagCapability trust-policy-derived role session-tag capability, when applicable
318
+ * @param mutablePrincipalTagKeys stored PrincipalTag keys whose values are not authoritative
269
319
  * @returns strict literal keys and key patterns
270
320
  */
271
- function makeStrictContextKeys(request, contextKeys, resourceTagsAreKnown, s3BucketOrObjectRequest) {
321
+ function makeStrictContextKeys(request, contextKeys, resourceTagsAreKnown, s3BucketOrObjectRequest, principalTagsAreUnknown, sessionTagCapability, mutablePrincipalTagKeys) {
272
322
  const strictContextKeys = [...knownContextKeys, ...(request.additionalStrictContextKeys ?? [])];
273
323
  if (!request.principal) {
274
324
  strictContextKeys.push(CONTEXT_KEYS.userId, CONTEXT_KEYS.assumedRoot, '/^aws:PrincipalTag\/.*/');
@@ -281,6 +331,15 @@ function makeStrictContextKeys(request, contextKeys, resourceTagsAreKnown, s3Buc
281
331
  strictContextKeys.push(CONTEXT_KEYS.assumedRoot);
282
332
  }
283
333
  }
334
+ if (!principalTagsAreUnknown && request.principal && isIamUserArn(request.principal)) {
335
+ strictContextKeys.push('/^aws:PrincipalTag\/.*/');
336
+ }
337
+ if (sessionTagCapability) {
338
+ const immutableTagPattern = immutablePrincipalTagPattern(sessionTagCapability);
339
+ if (immutableTagPattern) {
340
+ strictContextKeys.push(immutableTagPattern);
341
+ }
342
+ }
284
343
  if (request.action.startsWith('s3:')) {
285
344
  strictContextKeys.push('s3:DataAccessPointAccount', 's3:DataAccessPointArn');
286
345
  }
@@ -291,14 +350,43 @@ function makeStrictContextKeys(request, contextKeys, resourceTagsAreKnown, s3Buc
291
350
  strictContextKeys.push('/^s3:BucketTag\/.*/');
292
351
  }
293
352
  }
353
+ // There also may be other tag context keys, so add those too. Mutable role tags must not
354
+ // receive a strict constraint because iam-simulate merges constraints with true-winning semantics.
294
355
  for (const key of Object.keys(contextKeys)) {
295
- if (key.toLowerCase().includes('tag/')) {
356
+ if (isTagContextKey(key) &&
357
+ !(principalTagsAreUnknown && isPrincipalTagContextKey(key)) &&
358
+ !mutablePrincipalTagKeys.has(key)) {
296
359
  strictContextKeys.push(key);
297
360
  }
298
361
  }
299
362
  return strictContextKeys;
300
363
  }
364
+ /**
365
+ * Checks whether a context key's first slash-delimited namespace ends in `Tag`.
366
+ *
367
+ * @param key context-key name to inspect
368
+ * @returns true for tag context keys such as `aws:PrincipalTag/Department`
369
+ */
370
+ function isTagContextKey(key) {
371
+ const slashIndex = key.indexOf('/');
372
+ return (slashIndex >= 3 &&
373
+ key
374
+ .slice(slashIndex - 3, slashIndex + 1)
375
+ .localeCompare('tag/', undefined, { sensitivity: 'base' }) === 0);
376
+ }
301
377
  const awsSourceKeyPrefix = 'aws:source';
378
+ const awsPrincipalTagPrefix = 'aws:principaltag/';
379
+ /**
380
+ * Checks whether a context key is in the PrincipalTag namespace.
381
+ *
382
+ * @param key context-key name to inspect
383
+ * @returns true when the key starts with `aws:PrincipalTag/`, ignoring case
384
+ */
385
+ function isPrincipalTagContextKey(key) {
386
+ return (key
387
+ .slice(0, awsPrincipalTagPrefix.length)
388
+ .localeCompare(awsPrincipalTagPrefix, undefined, { sensitivity: 'base' }) === 0);
389
+ }
302
390
  const anonymousAbsentContextKeys = new Set([
303
391
  'aws:principalarn',
304
392
  'aws:principalaccount',
@@ -318,17 +406,80 @@ const anonymousAbsentContextKeys = new Set([
318
406
  '/^aws:principaltag\/.*/'
319
407
  ]);
320
408
  /**
321
- * Convert an iam-lens strict context key into an iam-simulate Discovery constraint.
409
+ * Derives role session-tag capability independently from tags stored on the role.
410
+ *
411
+ * This lookup applies only to Discovery simulations for IAM roles and assumed-role sessions. IAM
412
+ * user tags are handled separately as fully strict because users cannot receive role-session tags.
413
+ *
414
+ * @param collectClient the collect client used to load the canonical role trust policy
415
+ * @param simulationRequest the request whose principal-tag certainty is being modeled
416
+ * @param resolvedPrincipalArnForContext the canonical role ARN resolved for an assumed-role session
417
+ * @param principalIsCollected whether absent principal metadata is authoritative
418
+ * @returns the role's session-tag capability, or undefined when role metadata is not applicable
419
+ */
420
+ async function sessionTagCapabilityForRequest(collectClient, simulationRequest, resolvedPrincipalArnForContext, principalIsCollected) {
421
+ if (simulationRequest.simulationMode !== 'Discovery' || !principalIsCollected) {
422
+ return undefined;
423
+ }
424
+ const requestPrincipal = simulationRequest.principal;
425
+ const roleArn = requestPrincipal && isIamRoleArn(requestPrincipal)
426
+ ? requestPrincipal
427
+ : resolvedPrincipalArnForContext;
428
+ if (!roleArn || !isIamRoleArn(roleArn)) {
429
+ return undefined;
430
+ }
431
+ const accountId = splitArnParts(roleArn).accountId;
432
+ if (!accountId) {
433
+ return undefined;
434
+ }
435
+ const trustPolicy = await collectClient.getResourcePolicyForArn(roleArn, accountId);
436
+ return roleSessionTagCapability(trustPolicy);
437
+ }
438
+ /**
439
+ * Finds stored role-tag values that a session may override.
440
+ *
441
+ * Stored tags determine baseline presence, while the independent trust-policy capability determines
442
+ * mutability. A mutable stored tag is always present, but its effective session value is unknown.
443
+ *
444
+ * @param capability the role's session-tag capability, or undefined for non-role requests
445
+ * @param contextKeys generated and caller-provided request context
446
+ * @param customContextKeys authoritative caller-provided context values
447
+ * @returns existing non-custom PrincipalTag context-key names whose values are mutable
448
+ */
449
+ function mutableStoredPrincipalTagKeys(capability, contextKeys, customContextKeys) {
450
+ if (!capability || capability.type === 'none') {
451
+ return new Set();
452
+ }
453
+ const storedPrincipalTagKeys = Object.keys(contextKeys).filter((key) => isPrincipalTagContextKey(key) && !hasCustomContextKey(customContextKeys, key));
454
+ if (capability.type === 'any') {
455
+ return new Set(storedPrincipalTagKeys);
456
+ }
457
+ return new Set(storedPrincipalTagKeys.filter((key) => {
458
+ const tagKey = key.slice(awsPrincipalTagPrefix.length);
459
+ return roleSessionCanSetTagKey(capability, tagKey);
460
+ }));
461
+ }
462
+ /**
463
+ * Convert an iam-lens context key into an iam-simulate Discovery constraint.
464
+ *
465
+ * Caller-provided and explicitly strict keys remain fully authoritative. A stored PrincipalTag
466
+ * whose role session may override it has known presence but an unknown value. All other input keys
467
+ * are strict and continue through the existing special-case handling.
322
468
  *
323
469
  * @param keyName the literal context key or slash-delimited key pattern
324
470
  * @param simulationRequest the normalized request whose context certainty is modeled
471
+ * @param mutablePrincipalTagKeys stored PrincipalTag keys whose values are non-authoritative
325
472
  * @returns the Discovery constraint to pass to iam-simulate
326
473
  * @internal
327
474
  */
328
- export function discoveryConstraintForStrictKey(keyName, simulationRequest) {
329
- if (hasCustomContextKey(simulationRequest.customContextKeys, keyName)) {
475
+ export function discoveryConstraintForStrictKey(keyName, simulationRequest, mutablePrincipalTagKeys) {
476
+ if (hasCustomContextKey(simulationRequest.customContextKeys, keyName) ||
477
+ simulationRequest.additionalStrictContextKeys?.includes(keyName)) {
330
478
  return { keyName, presenceIsKnown: true, valueIsKnown: true };
331
479
  }
480
+ if (mutablePrincipalTagKeys?.has(keyName)) {
481
+ return { keyName, presenceIsKnown: true, valueIsKnown: false };
482
+ }
332
483
  if (simulationRequest.principal === undefined &&
333
484
  anonymousAbsentContextKeys.has(keyName.toLowerCase())) {
334
485
  return { keyName, presenceIsKnown: true, valueIsKnown: true };