Skip to content

Commit 2566017

Browse files
authored
feat(lambda): grant function permissions to an AWS organization (#19975)
Closes #19538, also fixes #20146. I combined them because they touch the same surface area and it would be too hairy to separate them out. See [lambda docs](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-xorginvoke) for this feature. Introduces functionality to grant permissions to an organization in the following ways: ```ts declare const fn = new lambda.Function; // grant to an organization fn.grantInvoke(iam.OrganizationPrincipal('o-xxxxxxxxxx'); // grant to an account in an organization fn.grantInvoke(iam.AccountPrincipal('123456789012').inOrganization('o-xxxxxxxxxx')); ``` ---- ### All Submissions: * [x] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/master/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/master/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent ee37ed5 commit 2566017

File tree

11 files changed

+630
-37
lines changed

11 files changed

+630
-37
lines changed

packages/@aws-cdk/aws-lambda/README.md

+55-13
Original file line numberDiff line numberDiff line change
@@ -155,12 +155,13 @@ if (fn.timeout) {
155155

156156
AWS Lambda supports resource-based policies for controlling access to Lambda
157157
functions and layers on a per-resource basis. In particular, this allows you to
158-
give permission to AWS services and other AWS accounts to modify and invoke your
159-
functions. You can also restrict permissions given to AWS services by providing
160-
a source account or ARN (representing the account and identifier of the resource
161-
that accesses the function or layer).
158+
give permission to AWS services, AWS Organizations, or other AWS accounts to
159+
modify and invoke your functions.
160+
161+
### Grant function access to AWS services
162162

163163
```ts
164+
// Grant permissions to a service
164165
declare const fn: lambda.Function;
165166
const principal = new iam.ServicePrincipal('my-service');
166167

@@ -172,10 +173,58 @@ fn.addPermission('my-service Invocation', {
172173
});
173174
```
174175

175-
For more information, see [Resource-based
176-
policies](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html)
176+
You can also restrict permissions given to AWS services by providing
177+
a source account or ARN (representing the account and identifier of the resource
178+
that accesses the function or layer).
179+
180+
For more information, see
181+
[Granting function access to AWS services](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-serviceinvoke)
182+
in the AWS Lambda Developer Guide.
183+
184+
### Grant function access to an AWS Organization
185+
186+
```ts
187+
// Grant permissions to an entire AWS organization
188+
declare const fn: lambda.Function;
189+
const org = new iam.OrganizationPrincipal('o-xxxxxxxxxx');
190+
191+
fn.grantInvoke(org);
192+
```
193+
194+
In the above example, the `principal` will be `*` and all users in the
195+
organization `o-xxxxxxxxxx` will get function invocation permissions.
196+
197+
You can restrict permissions given to the organization by specifying an
198+
AWS account or role as the `principal`:
199+
200+
```ts
201+
// Grant permission to an account ONLY IF they are part of the organization
202+
declare const fn: lambda.Function;
203+
const account = new iam.AccountPrincipal('123456789012');
204+
205+
fn.grantInvoke(account.inOrganization('o-xxxxxxxxxx'));
206+
```
207+
208+
For more information, see
209+
[Granting function access to an organization](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-xorginvoke)
177210
in the AWS Lambda Developer Guide.
178211

212+
### Grant function access to other AWS accounts
213+
214+
```ts
215+
// Grant permission to other AWS account
216+
declare const fn: lambda.Function;
217+
const account = new iam.AccountPrincipal('123456789012');
218+
219+
fn.grantInvoke(account);
220+
```
221+
222+
For more information, see
223+
[Granting function access to other accounts](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-xaccountinvoke)
224+
in the AWS Lambda Developer Guide.
225+
226+
### Grant function access to unowned principals
227+
179228
Providing an unowned principal (such as account principals, generic ARN
180229
principals, service principals, and principals in other accounts) to a call to
181230
`fn.grantInvoke` will result in a resource-based policy being created. If the
@@ -198,13 +247,6 @@ const servicePrincipalWithConditions = servicePrincipal.withConditions({
198247
});
199248

200249
fn.grantInvoke(servicePrincipalWithConditions);
201-
202-
// Equivalent to:
203-
fn.addPermission('my-service Invocation', {
204-
principal: servicePrincipal,
205-
sourceArn: sourceArn,
206-
sourceAccount: sourceAccount,
207-
});
208250
```
209251

210252
## Versions

packages/@aws-cdk/aws-lambda/lib/function-base.ts

+72-14
Original file line numberDiff line numberDiff line change
@@ -343,8 +343,10 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
343343
return;
344344
}
345345

346-
const principal = this.parsePermissionPrincipal(permission.principal);
347-
const { sourceAccount, sourceArn } = this.parseConditions(permission.principal) ?? {};
346+
let principal = this.parsePermissionPrincipal(permission.principal);
347+
348+
let { sourceArn, sourceAccount, principalOrgID } = this.validateConditionCombinations(permission.principal) ?? {};
349+
348350
const action = permission.action ?? 'lambda:InvokeFunction';
349351
const scope = permission.scope ?? this;
350352

@@ -357,6 +359,7 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
357359
eventSourceToken: permission.eventSourceToken,
358360
sourceAccount: permission.sourceAccount ?? sourceAccount,
359361
sourceArn: permission.sourceArn ?? sourceArn,
362+
principalOrgId: permission.organizationId ?? principalOrgID,
360363
functionUrlAuthType: permission.functionUrlAuthType,
361364
});
362365
}
@@ -552,7 +555,6 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
552555
private parsePermissionPrincipal(principal: iam.IPrincipal) {
553556
// Try some specific common classes first.
554557
// use duck-typing, not instance of
555-
// @deprecated: after v2, we can change these to 'instanceof'
556558
if ('wrapped' in principal) {
557559
// eslint-disable-next-line dot-notation
558560
principal = principal['wrapped'];
@@ -570,6 +572,15 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
570572
return (principal as iam.ArnPrincipal).arn;
571573
}
572574

575+
const stringEquals = matchSingleKey('StringEquals', principal.policyFragment.conditions);
576+
if (stringEquals) {
577+
const orgId = matchSingleKey('aws:PrincipalOrgID', stringEquals);
578+
if (orgId) {
579+
// we will move the organization id to the `principalOrgId` property of `Permissions`.
580+
return '*';
581+
}
582+
}
583+
573584
// Try a best-effort approach to support simple principals that are not any of the predefined
574585
// classes, but are simple enough that they will fit into the Permission model. Main target
575586
// here: imported Roles, Users, Groups.
@@ -584,17 +595,67 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
584595
}
585596

586597
throw new Error(`Invalid principal type for Lambda permission statement: ${principal.constructor.name}. ` +
587-
'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal');
598+
'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal, OrganizationPrincipal');
599+
600+
/**
601+
* Returns the value at the key if the object contains the key and nothing else. Otherwise,
602+
* returns undefined.
603+
*/
604+
function matchSingleKey(key: string, obj: Record<string, any>): any | undefined {
605+
if (Object.keys(obj).length !== 1) { return undefined; }
606+
607+
return obj[key];
608+
}
609+
588610
}
589611

590-
private parseConditions(principal: iam.IPrincipal): { sourceAccount: string, sourceArn: string } | null {
612+
private validateConditionCombinations(principal: iam.IPrincipal): {
613+
sourceArn: string | undefined,
614+
sourceAccount: string | undefined,
615+
principalOrgID: string | undefined,
616+
} | undefined {
617+
const conditions = this.validateConditions(principal);
618+
619+
if (!conditions) { return undefined; }
620+
621+
const sourceArn = conditions.ArnLike ? conditions.ArnLike['aws:SourceArn'] : undefined;
622+
const sourceAccount = conditions.StringEquals ? conditions.StringEquals['aws:SourceAccount'] : undefined;
623+
const principalOrgID = conditions.StringEquals ? conditions.StringEquals['aws:PrincipalOrgID'] : undefined;
624+
625+
// PrincipalOrgID cannot be combined with any other conditions
626+
if (principalOrgID && (sourceArn || sourceAccount)) {
627+
throw new Error('PrincipalWithConditions had unsupported condition combinations for Lambda permission statement: principalOrgID cannot be set with other conditions.');
628+
}
629+
630+
return {
631+
sourceArn,
632+
sourceAccount,
633+
principalOrgID,
634+
};
635+
}
636+
637+
private validateConditions(principal: iam.IPrincipal): iam.Conditions | undefined {
591638
if (this.isPrincipalWithConditions(principal)) {
592639
const conditions: iam.Conditions = principal.policyFragment.conditions;
593640
const conditionPairs = flatMap(
594641
Object.entries(conditions),
595642
([operator, conditionObjs]) => Object.keys(conditionObjs as object).map(key => { return { operator, key }; }),
596643
);
597-
const supportedPrincipalConditions = [{ operator: 'ArnLike', key: 'aws:SourceArn' }, { operator: 'StringEquals', key: 'aws:SourceAccount' }];
644+
645+
// These are all the supported conditions. Some combinations are not supported,
646+
// like only 'aws:SourceArn' or 'aws:PrincipalOrgID' and 'aws:SourceAccount'.
647+
// These will be validated through `this.validateConditionCombinations`.
648+
const supportedPrincipalConditions = [{
649+
operator: 'ArnLike',
650+
key: 'aws:SourceArn',
651+
},
652+
{
653+
operator: 'StringEquals',
654+
key: 'aws:SourceAccount',
655+
}, {
656+
operator: 'StringEquals',
657+
key: 'aws:PrincipalOrgID',
658+
}];
598659

599660
const unsupportedConditions = conditionPairs.filter(
600661
(condition) => !supportedPrincipalConditions.some(
@@ -603,21 +664,18 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
603664
);
604665

605666
if (unsupportedConditions.length == 0) {
606-
return {
607-
sourceAccount: conditions.StringEquals['aws:SourceAccount'],
608-
sourceArn: conditions.ArnLike['aws:SourceArn'],
609-
};
667+
return conditions;
610668
} else {
611669
throw new Error(`PrincipalWithConditions had unsupported conditions for Lambda permission statement: ${JSON.stringify(unsupportedConditions)}. ` +
612670
`Supported operator/condition pairs: ${JSON.stringify(supportedPrincipalConditions)}`);
613671
}
614-
} else {
615-
return null;
616672
}
673+
674+
return undefined;
617675
}
618676

619-
private isPrincipalWithConditions(principal: iam.IPrincipal): principal is iam.PrincipalWithConditions {
620-
return 'conditions' in principal;
677+
private isPrincipalWithConditions(principal: iam.IPrincipal): boolean {
678+
return Object.keys(principal.policyFragment.conditions).length > 0;
621679
}
622680
}
623681

packages/@aws-cdk/aws-lambda/lib/permission.ts

+23-7
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,22 @@ export interface Permission {
2323
* A unique token that must be supplied by the principal invoking the
2424
* function.
2525
*
26-
* @default The caller would not need to present a token.
26+
* @default - The caller would not need to present a token.
2727
*/
2828
readonly eventSourceToken?: string;
2929

3030
/**
3131
* The entity for which you are granting permission to invoke the Lambda
32-
* function. This entity can be any valid AWS service principal, such as
33-
* s3.amazonaws.com or sns.amazonaws.com, or, if you are granting
34-
* cross-account permission, an AWS account ID. For example, you might want
35-
* to allow a custom application in another AWS account to push events to
36-
* Lambda by invoking your function.
32+
* function. This entity can be any of the following:
3733
*
38-
* The principal can be either an AccountPrincipal or a ServicePrincipal.
34+
* - a valid AWS service principal, such as `s3.amazonaws.com` or `sns.amazonaws.com`
35+
* - an AWS account ID for cross-account permissions. For example, you might want
36+
* to allow a custom application in another AWS account to push events to
37+
* Lambda by invoking your function.
38+
* - an AWS organization principal to grant permissions to an entire organization.
39+
*
40+
* The principal can be an AccountPrincipal, an ArnPrincipal, a ServicePrincipal,
41+
* or an OrganizationPrincipal.
3942
*/
4043
readonly principal: iam.IPrincipal;
4144

@@ -67,6 +70,19 @@ export interface Permission {
6770
*/
6871
readonly sourceArn?: string;
6972

73+
/**
74+
* The organization you want to grant permissions to. Use this ONLY if you
75+
* need to grant permissions to a subset of the organization. If you want to
76+
* grant permissions to the entire organization, sending the organization principal
77+
* through the `principal` property will suffice.
78+
*
79+
* You can use this property to ensure that all source principals are owned by
80+
* a specific organization.
81+
*
82+
* @default - No organizationId
83+
*/
84+
readonly organizationId?: string;
85+
7086
/**
7187
* The authType for the function URL that you are granting permissions for.
7288
*

0 commit comments

Comments
 (0)