aws-iam-language-server 0.0.17 → 0.0.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aws-iam-language-server",
3
- "version": "0.0.17",
3
+ "version": "0.0.19",
4
4
  "type": "module",
5
5
  "bin": "./src/server.js",
6
6
  "publisher": "MichaelBarney",
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.
@@ -25,14 +25,12 @@ export class ActionValidator extends ElementValidator {
25
25
  }
26
26
  if (isRuleEnabled('DEPENDENT_ACTION')) {
27
27
  const allActions = this.#getExpandedActions();
28
- for (const action of expanded) {
29
- const actionDef = ServiceReference.getAction(action);
30
- if (!actionDef?.dependentActions)
31
- continue;
32
- const missing = actionDef.dependentActions.filter((dep) => !allActions.has(dep.toLowerCase()));
33
- if (missing.length > 0) {
34
- diagnostics.push(createDiagnostic('DEPENDENT_ACTION', `"${action}" requires dependent action${missing.length > 1 ? 's' : ''}: ${missing.join(', ')}`, value.range, DiagnosticSeverity.Warning));
35
- }
28
+ const actionDef = ServiceReference.getAction(value.text);
29
+ if (!actionDef?.dependentActions)
30
+ continue;
31
+ const missing = actionDef.dependentActions.filter((dep) => !allActions.has(dep.toLowerCase()));
32
+ if (missing.length > 0) {
33
+ diagnostics.push(createDiagnostic('DEPENDENT_ACTION', `"${actionDef.name}" requires dependent action(s): ${missing.join(', ')}`, value.range, DiagnosticSeverity.Warning));
36
34
  }
37
35
  }
38
36
  }
@@ -376,7 +376,69 @@ export class TreeHcl extends TreeBase {
376
376
  }
377
377
  if (current?.type !== 'object_elem')
378
378
  return false;
379
- return this.#getObjectElemKey(current) === 'Statement';
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
- return this.#getPairKeyText(pair) === 'Statement';
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' && this.#getPairKeyText(sequenceParent) === 'Statement';
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;