aws-iam-language-server 0.0.18 → 0.0.20
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/package.json +1 -1
- package/readme.md +4 -0
- package/src/handlers/completion/resource-value.js +3 -2
- package/src/lib/iam-policy/arn.d.ts +5 -0
- package/src/lib/iam-policy/arn.js +30 -1
- package/src/lib/treesitter/hcl.js +66 -1
- package/src/lib/treesitter/json.js +21 -1
- package/src/lib/treesitter/yaml.js +59 -2
package/package.json
CHANGED
package/readme.md
CHANGED
|
@@ -55,6 +55,10 @@ vim.lsp.enable("aws-iam-language-server")
|
|
|
55
55
|
|
|
56
56
|
## Features
|
|
57
57
|
|
|
58
|
+
This language server will detect policies within yaml/json documents, including deeply-nested policies.
|
|
59
|
+
This means it will work for polcies defined as CloudFormation resources or plain policy files.
|
|
60
|
+
Detection of a policy does require that you have a `Version` set to a valid version date: `2012-10-17` or `2008-10-17`).
|
|
61
|
+
|
|
58
62
|
### DocumentLink
|
|
59
63
|
|
|
60
64
|
Certain elements within a policy document will have a document link associated with it.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { CompletionItemKind, MarkupKind } from 'vscode-languageserver';
|
|
2
|
+
import { splitArn } from "../../lib/iam-policy/arn.js";
|
|
2
3
|
import { partitions } from "../../lib/iam-policy/partitions.js";
|
|
3
4
|
import { formatResourceDocumentation } from "../../lib/iam-policy/reference/documentation.js";
|
|
4
5
|
import { ServiceReference } from "../../lib/iam-policy/reference/services.js";
|
|
5
6
|
import { expandActionPattern } from "../../lib/iam-policy/wildcard.js";
|
|
6
7
|
import { partialRange } from "./index.js";
|
|
7
8
|
export function completeResourceValue(location, context) {
|
|
8
|
-
const parts = location.partial
|
|
9
|
+
const parts = splitArn(location.partial);
|
|
9
10
|
const range = partialRange(context.position, location.partial.length);
|
|
10
11
|
const items = [];
|
|
11
12
|
const statement = context.handler.getStatementContext(context.uri, context.position);
|
|
@@ -127,7 +128,7 @@ export function completeResourceValue(location, context) {
|
|
|
127
128
|
const resources = ServiceReference.getResourcesForActions(expandActionPattern(`${service}:*`));
|
|
128
129
|
for (const resource of resources) {
|
|
129
130
|
for (const arn of resource.arnFormats) {
|
|
130
|
-
const patternParts = arn
|
|
131
|
+
const patternParts = splitArn(arn);
|
|
131
132
|
const patternRegion = patternParts[3];
|
|
132
133
|
const patternAccount = patternParts[4];
|
|
133
134
|
if (region.length > 0 !== patternRegion.length > 0)
|
|
@@ -5,6 +5,11 @@ export type ArnParts = {
|
|
|
5
5
|
account: string;
|
|
6
6
|
resource: string;
|
|
7
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Split a string on `:` while preserving `${...}` placeholders intact.
|
|
10
|
+
* Colons inside `${...}` (e.g. `${AWS::AccountId}`) are not treated as delimiters.
|
|
11
|
+
*/
|
|
12
|
+
export declare function splitArn(arn: string): string[];
|
|
8
13
|
/**
|
|
9
14
|
* Parse an ARN string into its structural components.
|
|
10
15
|
* Returns null if the string is not a valid ARN structure (fewer than 6 colon-separated segments).
|
|
@@ -1,10 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Split a string on `:` while preserving `${...}` placeholders intact.
|
|
3
|
+
* Colons inside `${...}` (e.g. `${AWS::AccountId}`) are not treated as delimiters.
|
|
4
|
+
*/
|
|
5
|
+
export function splitArn(arn) {
|
|
6
|
+
const segments = [];
|
|
7
|
+
let current = '';
|
|
8
|
+
let depth = 0;
|
|
9
|
+
for (let i = 0; i < arn.length; i++) {
|
|
10
|
+
if (arn[i] === '$' && arn[i + 1] === '{') {
|
|
11
|
+
depth++;
|
|
12
|
+
current += '${';
|
|
13
|
+
i++;
|
|
14
|
+
}
|
|
15
|
+
else if (depth > 0 && arn[i] === '}') {
|
|
16
|
+
depth--;
|
|
17
|
+
current += '}';
|
|
18
|
+
}
|
|
19
|
+
else if (depth === 0 && arn[i] === ':') {
|
|
20
|
+
segments.push(current);
|
|
21
|
+
current = '';
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
current += arn[i];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
segments.push(current);
|
|
28
|
+
return segments;
|
|
29
|
+
}
|
|
1
30
|
/**
|
|
2
31
|
* Parse an ARN string into its structural components.
|
|
3
32
|
* Returns null if the string is not a valid ARN structure (fewer than 6 colon-separated segments).
|
|
4
33
|
* Everything after the 5th colon is treated as the resource portion.
|
|
5
34
|
*/
|
|
6
35
|
export function parseArn(arn) {
|
|
7
|
-
const segments = arn
|
|
36
|
+
const segments = splitArn(arn);
|
|
8
37
|
if (segments.length < 6)
|
|
9
38
|
return null;
|
|
10
39
|
if (segments[0] !== 'arn')
|
|
@@ -376,7 +376,69 @@ export class TreeHcl extends TreeBase {
|
|
|
376
376
|
}
|
|
377
377
|
if (current?.type !== 'object_elem')
|
|
378
378
|
return false;
|
|
379
|
-
|
|
379
|
+
if (this.#getObjectElemKey(current) !== 'Statement')
|
|
380
|
+
return false;
|
|
381
|
+
// The policy object is the parent of this object_elem
|
|
382
|
+
const policyObject = current.parent;
|
|
383
|
+
if (!policyObject || policyObject.type !== 'object')
|
|
384
|
+
return false;
|
|
385
|
+
return this.#hasValidJsonencodeVersion(policyObject);
|
|
386
|
+
}
|
|
387
|
+
#hasValidJsonencodeVersion(policyObject) {
|
|
388
|
+
const validVersions = new Set(['2012-10-17', '2008-10-17']);
|
|
389
|
+
for (const child of policyObject.namedChildren) {
|
|
390
|
+
if (child.type !== 'object_elem')
|
|
391
|
+
continue;
|
|
392
|
+
if (this.#getObjectElemKey(child) !== 'Version')
|
|
393
|
+
continue;
|
|
394
|
+
const expressions = child.namedChildren.filter((c) => c.type === 'expression');
|
|
395
|
+
const valueExpression = expressions.length >= 2 ? expressions[1] : null;
|
|
396
|
+
if (!valueExpression)
|
|
397
|
+
return false;
|
|
398
|
+
const values = this.#readExpressionStringValues(valueExpression);
|
|
399
|
+
return values.length === 1 && validVersions.has(values[0]);
|
|
400
|
+
}
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Check for a valid Version in an ERROR node's children for jsonencode mode.
|
|
405
|
+
* Looks for an object_elem with key "Version" and a valid version value, or
|
|
406
|
+
* an expression "Version" followed by an expression with a valid version string.
|
|
407
|
+
*/
|
|
408
|
+
#errorNodeHasValidJsonencodeVersion(errorNode) {
|
|
409
|
+
const validVersions = new Set(['2012-10-17', '2008-10-17']);
|
|
410
|
+
let sawVersionKey = false;
|
|
411
|
+
for (const child of errorNode.children) {
|
|
412
|
+
if (child.type === 'object_elem') {
|
|
413
|
+
const key = this.#getObjectElemKey(child);
|
|
414
|
+
if (key === 'Version') {
|
|
415
|
+
const expressions = child.namedChildren.filter((c) => c.type === 'expression');
|
|
416
|
+
const valueExpression = expressions.length >= 2 ? expressions[1] : null;
|
|
417
|
+
if (valueExpression) {
|
|
418
|
+
const values = this.#readExpressionStringValues(valueExpression);
|
|
419
|
+
if (values.length === 1 && validVersions.has(values[0]))
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
sawVersionKey = false;
|
|
424
|
+
}
|
|
425
|
+
else if (child.type === 'expression') {
|
|
426
|
+
const id = this.#getExpressionIdentifier(child);
|
|
427
|
+
if (id === 'Version') {
|
|
428
|
+
sawVersionKey = true;
|
|
429
|
+
}
|
|
430
|
+
else if (sawVersionKey) {
|
|
431
|
+
const values = this.#readExpressionStringValues(child);
|
|
432
|
+
if (values.length === 1 && validVersions.has(values[0]))
|
|
433
|
+
return true;
|
|
434
|
+
sawVersionKey = false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
sawVersionKey = false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
return false;
|
|
380
442
|
}
|
|
381
443
|
#buildJsonencodeCursorPath(cursorNode, statementObject, position) {
|
|
382
444
|
const keys = [];
|
|
@@ -803,6 +865,9 @@ export class TreeHcl extends TreeBase {
|
|
|
803
865
|
}
|
|
804
866
|
if (!foundStatement || !lastKey || !policyFormat)
|
|
805
867
|
return null;
|
|
868
|
+
// For jsonencode mode, require a valid Version in the ERROR node
|
|
869
|
+
if (policyFormat === 'standard' && !this.#errorNodeHasValidJsonencodeVersion(errorNode))
|
|
870
|
+
return null;
|
|
806
871
|
// Extract partial and value: when the key came from an attribute/object_elem
|
|
807
872
|
// that contains the quote (e.g., `effect = "`), they're empty. When the
|
|
808
873
|
// cursor node is inside a string (open quote slurped subsequent lines),
|
|
@@ -275,7 +275,27 @@ export class TreeJson extends TreeBase {
|
|
|
275
275
|
const pair = object.parent.parent;
|
|
276
276
|
if (pair?.type !== 'pair')
|
|
277
277
|
return false;
|
|
278
|
-
|
|
278
|
+
if (this.#getPairKeyText(pair) !== 'Statement')
|
|
279
|
+
return false;
|
|
280
|
+
const policyObject = pair.parent;
|
|
281
|
+
if (!policyObject || policyObject.type !== 'object')
|
|
282
|
+
return false;
|
|
283
|
+
return this.#hasValidVersion(policyObject);
|
|
284
|
+
}
|
|
285
|
+
#hasValidVersion(policyObject) {
|
|
286
|
+
const validVersions = new Set(['2012-10-17', '2008-10-17']);
|
|
287
|
+
for (const child of policyObject.namedChildren) {
|
|
288
|
+
if (child.type !== 'pair')
|
|
289
|
+
continue;
|
|
290
|
+
if (this.#getPairKeyText(child) !== 'Version')
|
|
291
|
+
continue;
|
|
292
|
+
const valueNode = child.namedChildren[1];
|
|
293
|
+
if (valueNode?.type !== 'string')
|
|
294
|
+
return false;
|
|
295
|
+
const content = valueNode.namedChildren.find((c) => c.type === 'string_content');
|
|
296
|
+
return content?.text ? validVersions.has(content.text) : false;
|
|
297
|
+
}
|
|
298
|
+
return false;
|
|
279
299
|
}
|
|
280
300
|
/**
|
|
281
301
|
* Get the text content of a pair's key (first named child string).
|
|
@@ -380,7 +380,9 @@ export class TreeYaml extends TreeBase {
|
|
|
380
380
|
*/
|
|
381
381
|
#isStatementSequence(sequence) {
|
|
382
382
|
const sequenceParent = this.#skipBlockNode(sequence.parent);
|
|
383
|
-
return sequenceParent?.type === 'block_mapping_pair' &&
|
|
383
|
+
return (sequenceParent?.type === 'block_mapping_pair' &&
|
|
384
|
+
this.#getPairKeyText(sequenceParent) === 'Statement' &&
|
|
385
|
+
this.#hasValidVersion(sequenceParent));
|
|
384
386
|
}
|
|
385
387
|
#resolveCursorContext(node, statementMapping, position) {
|
|
386
388
|
const cursorPath = this.#buildCursorPath(node, statementMapping, position);
|
|
@@ -705,7 +707,59 @@ export class TreeYaml extends TreeBase {
|
|
|
705
707
|
node = this.#skipBlockNode(node.parent);
|
|
706
708
|
if (node?.type !== 'block_mapping_pair')
|
|
707
709
|
return false;
|
|
708
|
-
return this.#getPairKeyText(node) === 'Statement';
|
|
710
|
+
return this.#getPairKeyText(node) === 'Statement' && this.#hasValidVersion(node);
|
|
711
|
+
}
|
|
712
|
+
/**
|
|
713
|
+
* Check that a Statement pair's parent block_mapping contains a Version pair
|
|
714
|
+
* with a valid IAM policy version value.
|
|
715
|
+
*/
|
|
716
|
+
#hasValidVersion(statementPair) {
|
|
717
|
+
const validVersions = new Set(['2012-10-17', '2008-10-17']);
|
|
718
|
+
const policyMapping = statementPair.parent;
|
|
719
|
+
if (!policyMapping || policyMapping.type !== 'block_mapping')
|
|
720
|
+
return false;
|
|
721
|
+
for (const child of policyMapping.namedChildren) {
|
|
722
|
+
if (child.type !== 'block_mapping_pair')
|
|
723
|
+
continue;
|
|
724
|
+
if (this.#getPairKeyText(child) !== 'Version')
|
|
725
|
+
continue;
|
|
726
|
+
const values = this.#readPairStringValues(child);
|
|
727
|
+
return values.length === 1 && validVersions.has(values[0]);
|
|
728
|
+
}
|
|
729
|
+
return false;
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Check for a valid Version in an ERROR node's children. Looks for a "Version"
|
|
733
|
+
* flow_node followed by a valid version string, or a block_mapping_pair with
|
|
734
|
+
* key "Version" and valid value.
|
|
735
|
+
*/
|
|
736
|
+
#errorNodeHasValidVersion(errorNode) {
|
|
737
|
+
const validVersions = new Set(['2012-10-17', '2008-10-17']);
|
|
738
|
+
let sawVersionKey = false;
|
|
739
|
+
for (const child of errorNode.children) {
|
|
740
|
+
if (child.type === 'block_mapping_pair') {
|
|
741
|
+
const key = this.#getPairKeyText(child);
|
|
742
|
+
if (key === 'Version') {
|
|
743
|
+
const values = this.#readPairStringValues(child);
|
|
744
|
+
if (values.length === 1 && validVersions.has(values[0]))
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
sawVersionKey = false;
|
|
748
|
+
}
|
|
749
|
+
else if (child.type === 'flow_node') {
|
|
750
|
+
const text = this.#getScalarText(child);
|
|
751
|
+
if (text === 'Version') {
|
|
752
|
+
sawVersionKey = true;
|
|
753
|
+
}
|
|
754
|
+
else if (sawVersionKey && text !== null && validVersions.has(text)) {
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
else if (text !== ':') {
|
|
758
|
+
sawVersionKey = false;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
return false;
|
|
709
763
|
}
|
|
710
764
|
/**
|
|
711
765
|
* Get the unquoted text of a block_mapping_pair's key.
|
|
@@ -805,6 +859,9 @@ export class TreeYaml extends TreeBase {
|
|
|
805
859
|
}
|
|
806
860
|
if (!errorNode)
|
|
807
861
|
return null;
|
|
862
|
+
// Pre-scan ERROR children for a valid Version pair
|
|
863
|
+
if (!this.#errorNodeHasValidVersion(errorNode))
|
|
864
|
+
return null;
|
|
808
865
|
// Scan ERROR children for a Statement-like structure: "Statement" + ":"
|
|
809
866
|
let foundStatement = false;
|
|
810
867
|
let lastKey = null;
|