@kensio/yulin 1.21.12 → 1.21.13

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 (23) hide show
  1. package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.d.ts +1 -1
  2. package/dist/service/dynamodb/command/authorize/sim-dynamodb-authorizer.js +23 -4
  3. package/dist/service/dynamodb/command/authorize/sim-dynamodb-leading-keys.d.ts +33 -0
  4. package/dist/service/dynamodb/command/authorize/sim-dynamodb-leading-keys.js +45 -0
  5. package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-get-item.js +2 -0
  6. package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-tables.d.ts +10 -2
  7. package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-tables.js +1 -1
  8. package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-write-item.js +7 -1
  9. package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-write.d.ts +6 -0
  10. package/dist/service/dynamodb/command/batch/sim-dynamodb-batch-write.js +8 -8
  11. package/dist/service/dynamodb/command/item/sim-dynamodb-delete-item.js +2 -1
  12. package/dist/service/dynamodb/command/item/sim-dynamodb-get-item.js +2 -1
  13. package/dist/service/dynamodb/command/item/sim-dynamodb-put-item.js +2 -1
  14. package/dist/service/dynamodb/command/item/sim-dynamodb-update-item.js +2 -1
  15. package/dist/service/dynamodb/command/query/sim-dynamodb-query.js +6 -1
  16. package/dist/service/dynamodb/command/table/sim-dynamodb-table-access.d.ts +16 -6
  17. package/dist/service/dynamodb/command/table/sim-dynamodb-table-access.js +34 -8
  18. package/dist/service/iam/authorize/match/condition/sim-iam-condition-operator.d.ts +4 -1
  19. package/dist/service/iam/authorize/match/condition/string/all-values/sim-iam-for-all-values-string-condition-operator.d.ts +5 -1
  20. package/dist/service/iam/authorize/match/condition/string/all-values/sim-iam-for-all-values-string-condition-operator.js +5 -1
  21. package/docs/services/dynamodb/README.md +108 -0
  22. package/docs/services/iam/README.md +8 -2
  23. package/package.json +2 -2
@@ -29,7 +29,7 @@ export declare class SimDynamoDbAuthorizer {
29
29
  * omitted caller, which defaults to Account root, from an explicit anonymous
30
30
  * caller.
31
31
  */
32
- authorizeTable(action: string, tableName: string, caller?: SimAwsCaller): SimAwsResolvedCaller;
32
+ authorizeTable(action: string, tableName: string, caller?: SimAwsCaller, leadingKeys?: readonly string[]): SimAwsResolvedCaller;
33
33
  /**
34
34
  * Ensure the caller may perform an action on a table's stream.
35
35
  *
@@ -1,5 +1,19 @@
1
1
  import { SimIamAccessDenied } from "../../../iam/error/sim-iam.error.js";
2
2
  import { simDynamoDbTableArn } from "../../table/sim-dynamodb-table-arn.js";
3
+ import { simDynamoDbLeadingKeysConditionKey } from "./sim-dynamodb-leading-keys.js";
4
+ /**
5
+ * The condition values a request carries for the table it names.
6
+ *
7
+ * A request reaching no partition key value supplies none rather than an empty
8
+ * list, leaving the key absent from the context as it is on AWS for an
9
+ * operation that names no item.
10
+ */
11
+ function conditionContextOf(leadingKeys) {
12
+ if (leadingKeys === undefined || leadingKeys.length === 0) {
13
+ return {};
14
+ }
15
+ return { [simDynamoDbLeadingKeysConditionKey]: leadingKeys };
16
+ }
3
17
  /**
4
18
  * Applies simulated IAM authorization to DynamoDB requests.
5
19
  *
@@ -26,8 +40,8 @@ export class SimDynamoDbAuthorizer {
26
40
  * omitted caller, which defaults to Account root, from an explicit anonymous
27
41
  * caller.
28
42
  */
29
- authorizeTable(action, tableName, caller) {
30
- return this.authorizeResource(action, simDynamoDbTableArn(this.accountRegionScope, tableName), caller);
43
+ authorizeTable(action, tableName, caller, leadingKeys) {
44
+ return this.authorizeResource(action, simDynamoDbTableArn(this.accountRegionScope, tableName), caller, leadingKeys);
31
45
  }
32
46
  /**
33
47
  * Ensure the caller may perform an action on a table's stream.
@@ -51,8 +65,13 @@ export class SimDynamoDbAuthorizer {
51
65
  authorizeAnyTable(action, caller) {
52
66
  return this.authorizeResource(action, "*", caller);
53
67
  }
54
- authorizeResource(action, resource, caller) {
55
- const decision = this.iam.authorize({ action, resource, caller });
68
+ authorizeResource(action, resource, caller, leadingKeys) {
69
+ const decision = this.iam.authorize({
70
+ action,
71
+ resource,
72
+ caller,
73
+ conditionContext: conditionContextOf(leadingKeys),
74
+ });
56
75
  if (decision.isDenied) {
57
76
  throw new SimIamAccessDenied({
58
77
  principal: decision.caller.principal,
@@ -0,0 +1,33 @@
1
+ import type { SimDynamoDbItem } from "../../item/sim-dynamodb-item.js";
2
+ import type { SimDynamoDbValue } from "../../item/sim-dynamodb-value.js";
3
+ import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
4
+ /**
5
+ * The condition key DynamoDB names a request's partition key values by.
6
+ */
7
+ export declare const simDynamoDbLeadingKeysConditionKey = "dynamodb:LeadingKeys";
8
+ /**
9
+ * The partition key values one request reaches in one table.
10
+ *
11
+ * Which attribute holds the partition key is the table's to say, so a command
12
+ * hands over how to read the values rather than the values themselves. The
13
+ * table is found before the caller is authorized against it, and the values
14
+ * are read from it in between.
15
+ */
16
+ export type SimDynamoDbLeadingKeys = (table: SimDynamoDbTable) => readonly string[];
17
+ /**
18
+ * Read the partition key values a run of items or keys carries.
19
+ *
20
+ * The items are read when authorization asks for them rather than at the call
21
+ * site, which is what keeps a request DynamoDB would refuse being refused
22
+ * after the caller has been authorized rather than before.
23
+ *
24
+ * An item with no readable partition key value is left out.
25
+ */
26
+ export declare function simDynamoDbLeadingKeysOf(items: () => readonly SimDynamoDbItem[]): SimDynamoDbLeadingKeys;
27
+ /**
28
+ * Read the one partition key value a key condition names.
29
+ *
30
+ * The value is already held against the key schema of what is being read, so
31
+ * the table says nothing more about it here.
32
+ */
33
+ export declare function simDynamoDbLeadingKeyOf(value: SimDynamoDbValue): readonly string[];
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The condition key DynamoDB names a request's partition key values by.
3
+ */
4
+ export const simDynamoDbLeadingKeysConditionKey = "dynamodb:LeadingKeys";
5
+ /**
6
+ * Write one partition key value the way an IAM policy condition holds it.
7
+ *
8
+ * A policy condition value is a string. A string key is its text and a number
9
+ * key is its digits. Binary has no form documented for the condition key, and
10
+ * a request carrying a binary partition key leaves the value out rather than
11
+ * inventing an encoding for it.
12
+ */
13
+ function leadingKeyText(value) {
14
+ if (value?.kind === "S") {
15
+ return value.text;
16
+ }
17
+ if (value?.kind === "N") {
18
+ return value.number.text;
19
+ }
20
+ return undefined;
21
+ }
22
+ /**
23
+ * Read the partition key values a run of items or keys carries.
24
+ *
25
+ * The items are read when authorization asks for them rather than at the call
26
+ * site, which is what keeps a request DynamoDB would refuse being refused
27
+ * after the caller has been authorized rather than before.
28
+ *
29
+ * An item with no readable partition key value is left out.
30
+ */
31
+ export function simDynamoDbLeadingKeysOf(items) {
32
+ return (table) => items()
33
+ .map((item) => leadingKeyText(item.attribute(table.keySchema.hashKeyAttributeName)))
34
+ .filter((text) => text !== undefined);
35
+ }
36
+ /**
37
+ * Read the one partition key value a key condition names.
38
+ *
39
+ * The value is already held against the key schema of what is being read, so
40
+ * the table says nothing more about it here.
41
+ */
42
+ export function simDynamoDbLeadingKeyOf(value) {
43
+ const text = leadingKeyText(value);
44
+ return text === undefined ? [] : [text];
45
+ }
@@ -2,6 +2,7 @@ import { readSimDynamoDbBatchReads, } from "./sim-dynamodb-batch-reads.js";
2
2
  import { assertDistinctBatchItems } from "./sim-dynamodb-batch-request-items.js";
3
3
  import { reachSimDynamoDbBatchTables, } from "./sim-dynamodb-batch-tables.js";
4
4
  import { refuseUnsimulatedBatchReadInput } from "./sim-dynamodb-unsimulated-batch-input.js";
5
+ import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
5
6
  /**
6
7
  * The BatchGetItem command.
7
8
  *
@@ -27,6 +28,7 @@ export class SimDynamoDbBatchGetItem {
27
28
  access: this.access,
28
29
  operation: "BatchGetItem",
29
30
  caller: options?.caller,
31
+ leadingKeys: (entry) => simDynamoDbLeadingKeysOf(() => entry.keys),
30
32
  });
31
33
  const responses = Object.fromEntries(reached.map((entry) => [entry.requested.reference, readItems(entry)]));
32
34
  // Nothing here throttles or stops at a response size, so no key is ever
@@ -1,6 +1,7 @@
1
1
  import type { SimAwsCaller } from "../../../aws/caller/sim-aws-caller.js";
2
2
  import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
3
3
  import type { SimDynamoDbTableAccess } from "../table/sim-dynamodb-table-access.js";
4
+ import type { SimDynamoDbLeadingKeys } from "../authorize/sim-dynamodb-leading-keys.js";
4
5
  /**
5
6
  * What one table of a batch asks for, against the table it names.
6
7
  */
@@ -8,10 +9,17 @@ export interface SimDynamoDbBatchTable<Requested> {
8
9
  readonly table: SimDynamoDbTable;
9
10
  readonly requested: Requested;
10
11
  }
11
- interface SimDynamoDbBatchReach {
12
+ interface SimDynamoDbBatchReach<Requested> {
12
13
  readonly access: SimDynamoDbTableAccess;
13
14
  readonly operation: string;
14
15
  readonly caller: SimAwsCaller | undefined;
16
+ /**
17
+ * How to read the partition key values one table of the batch is asked for.
18
+ *
19
+ * Each table is authorized on its own, so each carries the keys the batch
20
+ * names in that table and no others.
21
+ */
22
+ readonly leadingKeys: (requested: Requested) => SimDynamoDbLeadingKeys;
15
23
  }
16
24
  /**
17
25
  * Reach every table a batch names, authorizing the caller against each.
@@ -24,5 +32,5 @@ interface SimDynamoDbBatchReach {
24
32
  */
25
33
  export declare function reachSimDynamoDbBatchTables<Requested extends {
26
34
  readonly reference: string;
27
- }>(requested: readonly Requested[], reach: SimDynamoDbBatchReach): readonly SimDynamoDbBatchTable<Requested>[];
35
+ }>(requested: readonly Requested[], reach: SimDynamoDbBatchReach<Requested>): readonly SimDynamoDbBatchTable<Requested>[];
28
36
  export {};
@@ -11,7 +11,7 @@ import { SimDynamoDbValidationException } from "../../error/dynamodb.error.js";
11
11
  export function reachSimDynamoDbBatchTables(requested, reach) {
12
12
  const action = `dynamodb:${reach.operation}`;
13
13
  const reached = requested.map((entry) => ({
14
- table: reach.access.required(action, entry.reference, reach.caller),
14
+ table: reach.access.required(action, entry.reference, reach.caller, reach.leadingKeys(entry)),
15
15
  requested: entry,
16
16
  }));
17
17
  assertDistinctTables(reached, reach.operation);
@@ -2,6 +2,7 @@ import { assertDistinctBatchItems } from "./sim-dynamodb-batch-request-items.js"
2
2
  import { reachSimDynamoDbBatchTables } from "./sim-dynamodb-batch-tables.js";
3
3
  import { readSimDynamoDbBatchWrites } from "./sim-dynamodb-batch-writes.js";
4
4
  import { refuseUnsimulatedBatchWriteInput } from "./sim-dynamodb-unsimulated-batch-input.js";
5
+ import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
5
6
  const operation = "BatchWriteItem";
6
7
  /**
7
8
  * The BatchWriteItem command.
@@ -25,7 +26,12 @@ export class SimDynamoDbBatchWriteItem {
25
26
  handle(command, options) {
26
27
  const input = command.input;
27
28
  refuseUnsimulatedBatchWriteInput(input);
28
- const reached = reachSimDynamoDbBatchTables(readSimDynamoDbBatchWrites(input.RequestItems), { access: this.access, operation, caller: options?.caller });
29
+ const reached = reachSimDynamoDbBatchTables(readSimDynamoDbBatchWrites(input.RequestItems), {
30
+ access: this.access,
31
+ operation,
32
+ caller: options?.caller,
33
+ leadingKeys: (entry) => simDynamoDbLeadingKeysOf(() => entry.writes.map((write) => write.names)),
34
+ });
29
35
  // Marshalling every key checks it against the table's key schema, and is
30
36
  // what tells two operations on one item apart. Every table is checked
31
37
  // before any of them is written to.
@@ -1,3 +1,4 @@
1
+ import { SimDynamoDbItem } from "../../item/sim-dynamodb-item.js";
1
2
  import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
2
3
  import type { SimDynamoDbWriteRequest } from "./batch.command.js";
3
4
  /**
@@ -8,6 +9,11 @@ import type { SimDynamoDbWriteRequest } from "./batch.command.js";
8
9
  * before it applies anything, since the check and the write read the same item.
9
10
  */
10
11
  export interface SimDynamoDbBatchWrite {
12
+ /**
13
+ * The item or key this write names, which is where a policy condition reads
14
+ * its partition key value from.
15
+ */
16
+ readonly names: SimDynamoDbItem;
11
17
  /**
12
18
  * The primary key this write works on, as the table marshals it.
13
19
  */
@@ -5,15 +5,15 @@ import { readSimDynamoDbKey } from "../item/sim-dynamodb-key-input.js";
5
5
  * A put in a batch write, which replaces the whole item as PutItem does.
6
6
  */
7
7
  class SimDynamoDbBatchPut {
8
- item;
8
+ names;
9
9
  constructor(item) {
10
- this.item = item;
10
+ this.names = item;
11
11
  }
12
12
  keyIn(table) {
13
- return table.keyOfItem(this.item);
13
+ return table.keyOfItem(this.names);
14
14
  }
15
15
  applyTo(table) {
16
- table.putItem(this.item);
16
+ table.putItem(this.names);
17
17
  }
18
18
  }
19
19
  /**
@@ -21,15 +21,15 @@ class SimDynamoDbBatchPut {
21
21
  * that is already free is deleted successfully.
22
22
  */
23
23
  class SimDynamoDbBatchDelete {
24
- key;
24
+ names;
25
25
  constructor(key) {
26
- this.key = key;
26
+ this.names = key;
27
27
  }
28
28
  keyIn(table) {
29
- return table.keyOfKey(this.key);
29
+ return table.keyOfKey(this.names);
30
30
  }
31
31
  applyTo(table) {
32
- table.deleteItem(this.key);
32
+ table.deleteItem(this.names);
33
33
  }
34
34
  }
35
35
  /**
@@ -1,3 +1,4 @@
1
+ import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
1
2
  import { readSimDynamoDbKey } from "./sim-dynamodb-key-input.js";
2
3
  import { SimDynamoDbConditionCheck } from "./sim-dynamodb-condition-check.js";
3
4
  import { SimDynamoDbReturnValues } from "./sim-dynamodb-return-values.js";
@@ -26,7 +27,7 @@ export class SimDynamoDbDeleteItem {
26
27
  // The condition is read before the table is reached, so an expression
27
28
  // DynamoDB would refuse is refused whether or not the key holds anything.
28
29
  const check = SimDynamoDbConditionCheck.read(input, "DeleteItem");
29
- const table = this.access.required("dynamodb:DeleteItem", input.TableName, options?.caller);
30
+ const table = this.access.required("dynamodb:DeleteItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readSimDynamoDbKey(input.Key)]));
30
31
  const asked = SimDynamoDbReturnValues.read(input.ReturnValues, "DeleteItem");
31
32
  const key = readSimDynamoDbKey(input.Key);
32
33
  check.assertHoldsFor(table.getItem(key));
@@ -1,3 +1,4 @@
1
+ import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
1
2
  import { readSimDynamoDbKey } from "./sim-dynamodb-key-input.js";
2
3
  import { refuseUnsimulatedItemReadInput } from "./sim-dynamodb-unsimulated-item-read-input.js";
3
4
  import { readSimDynamoDbProjection } from "../../expression/projection/sim-dynamodb-projection-expression.js";
@@ -26,7 +27,7 @@ export class SimDynamoDbGetItem {
26
27
  // The projection is read before the table is reached, so an expression
27
28
  // DynamoDB would refuse is refused whether or not the key holds anything.
28
29
  const projection = readSimDynamoDbProjection(input);
29
- const table = this.access.required("dynamodb:GetItem", input.TableName, options?.caller);
30
+ const table = this.access.required("dynamodb:GetItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readSimDynamoDbKey(input.Key)]));
30
31
  const found = table.getItem(readSimDynamoDbKey(input.Key));
31
32
  if (found === undefined) {
32
33
  return { $metadata: {} };
@@ -1,5 +1,6 @@
1
1
  import { SimDynamoDbValidationException } from "../../error/dynamodb.error.js";
2
2
  import { SimDynamoDbItem } from "../../item/sim-dynamodb-item.js";
3
+ import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
3
4
  import { SimDynamoDbConditionCheck } from "./sim-dynamodb-condition-check.js";
4
5
  import { SimDynamoDbReturnValues } from "./sim-dynamodb-return-values.js";
5
6
  import { refuseUnsimulatedItemWriteInput } from "./sim-dynamodb-unsimulated-item-write-input.js";
@@ -36,7 +37,7 @@ export class SimDynamoDbPutItem {
36
37
  // The condition is read before the table is reached, so an expression
37
38
  // DynamoDB would refuse is refused whether or not the key holds anything.
38
39
  const check = SimDynamoDbConditionCheck.read(input, "PutItem");
39
- const table = this.access.required("dynamodb:PutItem", input.TableName, options?.caller);
40
+ const table = this.access.required("dynamodb:PutItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readItem(input)]));
40
41
  const asked = SimDynamoDbReturnValues.read(input.ReturnValues, "PutItem");
41
42
  const item = readItem(input);
42
43
  check.assertHoldsFor(table.itemUnder(item));
@@ -1,3 +1,4 @@
1
+ import { simDynamoDbLeadingKeysOf } from "../authorize/sim-dynamodb-leading-keys.js";
1
2
  import { readSimDynamoDbKey } from "./sim-dynamodb-key-input.js";
2
3
  import { SimDynamoDbReturnValues } from "./sim-dynamodb-return-values.js";
3
4
  import { refuseUnsimulatedItemUpdateInput } from "./sim-dynamodb-unsimulated-item-update-input.js";
@@ -28,7 +29,7 @@ export class SimDynamoDbUpdateItem {
28
29
  // The expressions are read before the table is reached, so one DynamoDB
29
30
  // would refuse is refused whether or not the key holds anything.
30
31
  const plan = SimDynamoDbUpdatePlan.read(input, "UpdateItem");
31
- const table = this.access.required("dynamodb:UpdateItem", input.TableName, options?.caller);
32
+ const table = this.access.required("dynamodb:UpdateItem", input.TableName, options?.caller, simDynamoDbLeadingKeysOf(() => [readSimDynamoDbKey(input.Key)]));
32
33
  const asked = SimDynamoDbReturnValues.readForUpdate(input.ReturnValues, "UpdateItem");
33
34
  const key = readSimDynamoDbKey(input.Key);
34
35
  const existing = table.getItem(key);
@@ -2,6 +2,7 @@ import { SimDynamoDbItemPage } from "../item/sim-dynamodb-item-page.js";
2
2
  import { SimDynamoDbReadAnswer } from "../read/sim-dynamodb-read-answer.js";
3
3
  import { assertSimDynamoDbConsistentReadAnswerable } from "../read/sim-dynamodb-consistent-read.js";
4
4
  import { SimDynamoDbSelect } from "../read/sim-dynamodb-select.js";
5
+ import { simDynamoDbLeadingKeyOf } from "../authorize/sim-dynamodb-leading-keys.js";
5
6
  import { readSimDynamoDbQueryExpressions } from "./sim-dynamodb-query-expressions.js";
6
7
  import { refuseSimDynamoDbQuerySegment } from "./sim-dynamodb-query-segment.js";
7
8
  import { readSimDynamoDbQueryStartKey } from "./sim-dynamodb-query-start-key.js";
@@ -30,7 +31,11 @@ export class SimDynamoDbQuery {
30
31
  refuseUnsimulatedQueryInput(input);
31
32
  refuseSimDynamoDbQuerySegment(input);
32
33
  const expressions = readSimDynamoDbQueryExpressions(input);
33
- const table = this.access.required("dynamodb:Query", input.TableName, options?.caller);
34
+ // The one partition key the query reads is what authorization needs, and
35
+ // reading it needs the view rather than the table, so it is read again
36
+ // here against whichever of the two the request names.
37
+ const table = this.access.required("dynamodb:Query", input.TableName, options?.caller, (reached) => simDynamoDbLeadingKeyOf(expressions.terms.forTable(reached.view(input.IndexName))
38
+ .partitionKeyValue));
34
39
  // What is being read is settled here: the table, or one of its indexes. An
35
40
  // IndexName the table does not have is refused rather than read as the
36
41
  // table. Everything after this asks the view rather than the table, so a
@@ -4,6 +4,7 @@ import type { SimDynamoDbTable } from "../../table/sim-dynamodb-table.js";
4
4
  import type { SimDynamoDbTableName } from "../../table/sim-dynamodb-table-name.js";
5
5
  import type { SimDynamoDbTableStore } from "../../table/sim-dynamodb-table-store.js";
6
6
  import type { SimDynamoDbAuthorizer } from "../authorize/sim-dynamodb-authorizer.js";
7
+ import type { SimDynamoDbLeadingKeys } from "../authorize/sim-dynamodb-leading-keys.js";
7
8
  interface SimDynamoDbTableAccessProperties {
8
9
  readonly tables: SimDynamoDbTableStore;
9
10
  readonly authorizer: SimDynamoDbAuthorizer;
@@ -14,9 +15,14 @@ interface SimDynamoDbTableAccessProperties {
14
15
  *
15
16
  * Every table command goes through the same two steps in the same order: read
16
17
  * the name or ARN the request carries, then authorize the caller against it
17
- * before anything looks the table up. Keeping them here is what makes that
18
- * order the same for all of them, so no command can accidentally tell an
19
- * unauthorized caller which table names are taken.
18
+ * before the command is told whether the table is there. Keeping them here is
19
+ * what makes that order the same for all of them, so no command can
20
+ * accidentally tell an unauthorized caller which table names are taken.
21
+ *
22
+ * A command reaching particular items hands over how to read their partition
23
+ * key values, which authorization needs the table to read. The table is found
24
+ * first for that, and a caller refused against a table that is not there still
25
+ * hears AccessDenied rather than a missing table.
20
26
  */
21
27
  export declare class SimDynamoDbTableAccess {
22
28
  private readonly tables;
@@ -28,13 +34,17 @@ export declare class SimDynamoDbTableAccess {
28
34
  */
29
35
  reference(tableName: string | undefined): SimDynamoDbTableName;
30
36
  /**
31
- * Find the table a request names, refusing the caller before looking it up.
37
+ * Find the table a request names, refusing the caller before answering.
32
38
  */
33
- required(action: string, tableName: string | undefined, caller: SimAwsCaller | undefined): SimDynamoDbTable;
39
+ required(action: string, tableName: string | undefined, caller: SimAwsCaller | undefined, leadingKeys?: SimDynamoDbLeadingKeys): SimDynamoDbTable;
34
40
  /**
35
41
  * Find a table by a name that has already been read.
42
+ *
43
+ * A caller with no permission hears AccessDenied whether or not the table is
44
+ * there, which keeps an unauthorized caller from finding out which table
45
+ * names are taken.
36
46
  */
37
- requiredByName(action: string, name: SimDynamoDbTableName, caller: SimAwsCaller | undefined): SimDynamoDbTable;
47
+ requiredByName(action: string, name: SimDynamoDbTableName, caller: SimAwsCaller | undefined, leadingKeys?: SimDynamoDbLeadingKeys): SimDynamoDbTable;
38
48
  /**
39
49
  * Ensure the caller may perform an action that names no particular table.
40
50
  */
@@ -1,13 +1,35 @@
1
1
  import { SimDynamoDbResourceNotFoundException } from "../../error/dynamodb.error.js";
2
2
  import { readSimDynamoDbTableReference } from "../../table/sim-dynamodb-table-reference.js";
3
+ /**
4
+ * Read the partition key values a request reaches, or none where they cannot
5
+ * be read.
6
+ *
7
+ * Authorization runs ahead of the checks a command makes on what it was given,
8
+ * as it does on AWS. A request whose key or key condition is malformed is
9
+ * authorized carrying no values and refused by the check that follows, so
10
+ * whatever reading them throws is dropped here.
11
+ */
12
+ function readLeadingKeys(leadingKeys, table) {
13
+ try {
14
+ return leadingKeys?.(table);
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
3
20
  /**
4
21
  * How a command reaches the table a request names.
5
22
  *
6
23
  * Every table command goes through the same two steps in the same order: read
7
24
  * the name or ARN the request carries, then authorize the caller against it
8
- * before anything looks the table up. Keeping them here is what makes that
9
- * order the same for all of them, so no command can accidentally tell an
10
- * unauthorized caller which table names are taken.
25
+ * before the command is told whether the table is there. Keeping them here is
26
+ * what makes that order the same for all of them, so no command can
27
+ * accidentally tell an unauthorized caller which table names are taken.
28
+ *
29
+ * A command reaching particular items hands over how to read their partition
30
+ * key values, which authorization needs the table to read. The table is found
31
+ * first for that, and a caller refused against a table that is not there still
32
+ * hears AccessDenied rather than a missing table.
11
33
  */
12
34
  export class SimDynamoDbTableAccess {
13
35
  tables;
@@ -25,17 +47,21 @@ export class SimDynamoDbTableAccess {
25
47
  return readSimDynamoDbTableReference(tableName, this.accountRegionScope);
26
48
  }
27
49
  /**
28
- * Find the table a request names, refusing the caller before looking it up.
50
+ * Find the table a request names, refusing the caller before answering.
29
51
  */
30
- required(action, tableName, caller) {
31
- return this.requiredByName(action, this.reference(tableName), caller);
52
+ required(action, tableName, caller, leadingKeys) {
53
+ return this.requiredByName(action, this.reference(tableName), caller, leadingKeys);
32
54
  }
33
55
  /**
34
56
  * Find a table by a name that has already been read.
57
+ *
58
+ * A caller with no permission hears AccessDenied whether or not the table is
59
+ * there, which keeps an unauthorized caller from finding out which table
60
+ * names are taken.
35
61
  */
36
- requiredByName(action, name, caller) {
37
- this.authorizer.authorizeTable(action, name.value, caller);
62
+ requiredByName(action, name, caller, leadingKeys) {
38
63
  const table = this.tables.find(name);
64
+ this.authorizer.authorizeTable(action, name.value, caller, table === undefined ? undefined : readLeadingKeys(leadingKeys, table));
39
65
  if (table === undefined) {
40
66
  throw new SimDynamoDbResourceNotFoundException(`No DynamoDB Table named ${name.value}`);
41
67
  }
@@ -15,7 +15,10 @@ export interface SimIamConditionOperator {
15
15
  * there is none for the policy value to equal.
16
16
  *
17
17
  * A `ForAnyValue` operator answers false whatever it wraps, because no
18
- * request value is there to satisfy it. AWS documents both rules.
18
+ * request value is there to satisfy it. A `ForAllValues` operator answers
19
+ * true, because every value the request carries matches vacuously. AWS
20
+ * documents all three rules, and warns that the `ForAllValues` rule leaves
21
+ * an `Allow` overly permissive without a `Null` guard beside it.
19
22
  */
20
23
  readonly matchesAbsentKey: boolean;
21
24
  /**
@@ -2,9 +2,13 @@ import type { SimIamConditionValue } from "../../../../../policy/sim-iam-policy.
2
2
  import type { SimIamConditionOperator } from "../../sim-iam-condition-operator.js";
3
3
  /**
4
4
  * Base for `ForAllValues` IAM string operators.
5
+ *
6
+ * An absent context key matches. AWS answers true where the request carries
7
+ * no value for the key. A `ForAllValues` `Allow` therefore needs a `Null`
8
+ * guard beside it to stay tight.
5
9
  */
6
10
  export declare abstract class SimIamForAllValuesStringConditionOperator implements SimIamConditionOperator {
7
- readonly matchesAbsentKey = false;
11
+ readonly matchesAbsentKey = true;
8
12
  /**
9
13
  * Check whether a given value matches the expected value.
10
14
  */
@@ -1,9 +1,13 @@
1
1
  import { simIamStringValues } from "../sim-iam-string-values.js";
2
2
  /**
3
3
  * Base for `ForAllValues` IAM string operators.
4
+ *
5
+ * An absent context key matches. AWS answers true where the request carries
6
+ * no value for the key. A `ForAllValues` `Allow` therefore needs a `Null`
7
+ * guard beside it to stay tight.
4
8
  */
5
9
  export class SimIamForAllValuesStringConditionOperator {
6
- matchesAbsentKey = false;
10
+ matchesAbsentKey = true;
7
11
  /**
8
12
  * Check whether a given value matches the expected value.
9
13
  */
@@ -3637,6 +3637,101 @@ unauthorized caller cannot find out which names are taken.
3637
3637
  in the same way, each against the `dynamodb:` action of its own name. `ListTables` names no table.
3638
3638
  It authorizes against `*`.
3639
3639
 
3640
+ ### Fine-grained access control with dynamodb:LeadingKeys
3641
+
3642
+ `GetItem`, `BatchGetItem`, `Query`, `PutItem`, `UpdateItem`, `DeleteItem` and `BatchWriteItem`
3643
+ supply the partition key values they reach as `dynamodb:LeadingKeys`. A policy conditioned on that
3644
+ key restricts a caller to the items under particular partition keys. AWS scopes one user of a
3645
+ shared table to their own rows this way.
3646
+
3647
+ The condition takes the `ForAllValues:` qualifier, as AWS requires for this key. Every partition key
3648
+ value the request reaches has to match. A batch or a write naming one item outside the allowed set
3649
+ is refused whole, and nothing it asked for is applied.
3650
+
3651
+ ```typescript sim-dynamodb-leading-keys
3652
+ import { CreateTableCommand, PutItemCommand } from "@aws-sdk/client-dynamodb";
3653
+ import { CreateRoleCommand, PutRolePolicyCommand } from "@aws-sdk/client-iam";
3654
+ import { SimAws } from "@kensio/yulin";
3655
+
3656
+ const simAws = new SimAws();
3657
+ const accountId = simAws.defaultAccountId;
3658
+ const region = simAws.defaultRegionName;
3659
+
3660
+ await simAws.dynamoDb().createTable(
3661
+ new CreateTableCommand({
3662
+ TableName: "OrdersTable",
3663
+ KeySchema: [{ AttributeName: "customerId", KeyType: "HASH" }],
3664
+ AttributeDefinitions: [{ AttributeName: "customerId", AttributeType: "S" }],
3665
+ BillingMode: "PAY_PER_REQUEST",
3666
+ }),
3667
+ );
3668
+
3669
+ const roleCreation = await simAws.iam().createRole(
3670
+ new CreateRoleCommand({
3671
+ RoleName: "CustomerRole",
3672
+ AssumeRolePolicyDocument: JSON.stringify({
3673
+ Version: "2012-10-17",
3674
+ Statement: {
3675
+ Effect: "Allow",
3676
+ Principal: { AWS: `arn:aws:iam::${accountId}:root` },
3677
+ Action: "sts:AssumeRole",
3678
+ },
3679
+ }),
3680
+ }),
3681
+ );
3682
+
3683
+ await simAws.iam().putRolePolicy(
3684
+ new PutRolePolicyCommand({
3685
+ RoleName: "CustomerRole",
3686
+ PolicyName: "OwnItemsOnly",
3687
+ PolicyDocument: JSON.stringify({
3688
+ Version: "2012-10-17",
3689
+ Statement: {
3690
+ Effect: "Allow",
3691
+ Action: "dynamodb:PutItem",
3692
+ Resource: `arn:aws:dynamodb:${region}:${accountId}:table/OrdersTable`,
3693
+ Condition: {
3694
+ "ForAllValues:StringEquals": { "dynamodb:LeadingKeys": ["c-1"] },
3695
+ },
3696
+ },
3697
+ }),
3698
+ }),
3699
+ );
3700
+
3701
+ const caller = { kind: "arn", arn: roleCreation.Role.Arn } as const;
3702
+
3703
+ // The customer's own item is written.
3704
+ await simAws.dynamoDb().putItem(
3705
+ new PutItemCommand({
3706
+ TableName: "OrdersTable",
3707
+ Item: { customerId: { S: "c-1" }, total: { N: "25" } },
3708
+ }),
3709
+ { caller },
3710
+ );
3711
+
3712
+ // Another customer's item is refused.
3713
+ try {
3714
+ await simAws.dynamoDb().putItem(
3715
+ new PutItemCommand({
3716
+ TableName: "OrdersTable",
3717
+ Item: { customerId: { S: "c-2" }, total: { N: "25" } },
3718
+ }),
3719
+ { caller },
3720
+ );
3721
+ } catch (error) {
3722
+ console.log(error instanceof Error ? error.name : "unknown error");
3723
+ // "AccessDenied"
3724
+ }
3725
+ ```
3726
+
3727
+ Reading the partition key values needs the table's key schema. The table is found first for that,
3728
+ before the caller is authorized against it. A caller with no permission still hears `AccessDenied`
3729
+ when the table is missing, and an unauthorized caller cannot find out which table names are taken.
3730
+
3731
+ A request carrying no readable partition key value leaves the key out of the condition context. A
3732
+ `ForAllValues:` condition matches a request carrying no value for its key, as it does on AWS. The
3733
+ condition allows such a request.
3734
+
3640
3735
  A transaction is authorized as the operations it is made of rather than as itself. Each action of a
3641
3736
  `TransactWriteItems` needs `dynamodb:PutItem`, `dynamodb:UpdateItem`, `dynamodb:DeleteItem` or
3642
3737
  `dynamodb:ConditionCheckItem` against the table it names, and each `Get` of a `TransactGetItems`
@@ -3657,6 +3752,9 @@ is written.
3657
3752
  answering with the attributes it projects, and paging with a `LastEvaluatedKey` carrying the index
3658
3753
  key and the table key together. A local secondary index also answers a strongly consistent read,
3659
3754
  and fetches an unprojected attribute from the base table.
3755
+ - `dynamodb:LeadingKeys` on `GetItem`, `BatchGetItem`, `Query`, `PutItem`, `UpdateItem`,
3756
+ `DeleteItem` and `BatchWriteItem`, carrying the partition key values the request reaches so a
3757
+ `ForAllValues:` condition can scope a caller to particular items.
3660
3758
  - `DescribeTable`, answering with the full table description, by table name or ARN.
3661
3759
  - `ListTables`, ordered by UTF-8 bytes and paged with `Limit` and `ExclusiveStartTableName`.
3662
3760
  - `DeleteTable`, following the table status DynamoDB moves a deleted table through, and refusing a
@@ -3730,6 +3828,16 @@ Arn`, `Fn::GetAtt … StreamArn` and `Fn::GetAtt … TableId` answering. A CDK `
3730
3828
 
3731
3829
  ## Limitations
3732
3830
 
3831
+ - `dynamodb:LeadingKeys` is the only DynamoDB condition key supplied. `dynamodb:Attributes`,
3832
+ `dynamodb:Select` and `dynamodb:ReturnValues` are absent from the condition context, and a
3833
+ statement conditioned on one of them matches no request. The transactional operations supply no
3834
+ leading keys either, and are authorized by action and table alone.
3835
+ - A partition key value reaches a policy condition as a string for a `S` key and as its digits for
3836
+ an `N` key. A binary partition key has no documented form for the condition key. A request
3837
+ carrying one leaves the value out.
3838
+ - A request against a table that is not there is authorized carrying no leading keys, since there is
3839
+ no key schema to read them with. A `ForAllValues:` condition matches such a request. A caller
3840
+ barred only by that condition learns the table is missing.
3733
3841
  - The document client's PartiQL Commands go unconverted, because PartiQL is an operation this
3734
3842
  simulation lacks yet. `ExecuteStatementCommand`, `BatchExecuteStatementCommand` and
3735
3843
  `ExecuteTransactionCommand` are refused by name, never half converted.
@@ -335,6 +335,11 @@ matches instead, as AWS documents. With no value in the request there is none fo
335
335
  to equal. A `ForAnyValue:` operator answers false for an absent key whatever it wraps, because no
336
336
  request value is there to satisfy it.
337
337
 
338
+ A `ForAllValues:` operator answers true for an absent key, which is also AWS behaviour. Every value
339
+ the request carries matches when it carries none. AWS warns that this leaves a `ForAllValues:`
340
+ `Allow` overly permissive, and asks for a `Null` check with a `false` value beside it. `Null` is
341
+ absent from the operators above. A statement written that way fails closed here.
342
+
338
343
  ### Statements left unevaluated
339
344
 
340
345
  An operator from outside the list above fails closed. The statement holding it matches nothing, and
@@ -1684,8 +1689,9 @@ Sim IAM models the policy behaviour that multi-service tests most commonly need.
1684
1689
  - Only the condition operators listed above are supported. A statement using any other operator
1685
1690
  fails closed, matching no request. `decision.unevaluatedStatements` names those statements, and a
1686
1691
  test can assert that a decision was reached over policies read in full
1687
- - A positive `ForAllValues:` condition fails to match a request carrying no value for the key, and
1688
- fails to match an empty value set. AWS matches both, and the negated form here matches both
1692
+ - A positive `ForAllValues:` condition fails to match an empty value set, where AWS matches one. An
1693
+ absent context key matches, as it does on AWS. A service here leaves a key out where the request
1694
+ carries no value for it, and the empty set is reached only by a caller passing one to `authorize`
1689
1695
  - Signature age is deliberately not enforced. `X-Amz-Date` must be present, well formed, and agree
1690
1696
  with the credential scope date, but is never compared to a clock. A client stamping real time can
1691
1697
  therefore reach a simulation keeping a different one. Session expiry _is_ enforced, against
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.12",
3
+ "version": "1.21.13",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",
@@ -290,7 +290,7 @@
290
290
  "conventional-changelog-conventionalcommits": "9.3.1",
291
291
  "esbuild": "^0.28.2",
292
292
  "eslint": "^10.8.0",
293
- "eslint-plugin-jsdoc": "^63.3.2",
293
+ "eslint-plugin-jsdoc": "^64.0.1",
294
294
  "eslint-plugin-no-secrets": "^2.3.3",
295
295
  "eslint-plugin-security": "^4.0.1",
296
296
  "eslint-plugin-unicorn": "^73.0.0",