Skip to content

Commit

Permalink
feat(lambda): grant function permissions to an AWS organization (#19975)
Browse files Browse the repository at this point in the history
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*
  • Loading branch information
kaizencc authored Jun 28, 2022
1 parent ee37ed5 commit 2566017
Show file tree
Hide file tree
Showing 11 changed files with 630 additions and 37 deletions.
68 changes: 55 additions & 13 deletions packages/@aws-cdk/aws-lambda/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,12 +155,13 @@ if (fn.timeout) {

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

### Grant function access to AWS services

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

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

For more information, see [Resource-based
policies](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html)
You can also restrict permissions given to AWS services by providing
a source account or ARN (representing the account and identifier of the resource
that accesses the function or layer).

For more information, see
[Granting function access to AWS services](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-serviceinvoke)
in the AWS Lambda Developer Guide.

### Grant function access to an AWS Organization

```ts
// Grant permissions to an entire AWS organization
declare const fn: lambda.Function;
const org = new iam.OrganizationPrincipal('o-xxxxxxxxxx');

fn.grantInvoke(org);
```

In the above example, the `principal` will be `*` and all users in the
organization `o-xxxxxxxxxx` will get function invocation permissions.

You can restrict permissions given to the organization by specifying an
AWS account or role as the `principal`:

```ts
// Grant permission to an account ONLY IF they are part of the organization
declare const fn: lambda.Function;
const account = new iam.AccountPrincipal('123456789012');

fn.grantInvoke(account.inOrganization('o-xxxxxxxxxx'));
```

For more information, see
[Granting function access to an organization](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-xorginvoke)
in the AWS Lambda Developer Guide.

### Grant function access to other AWS accounts

```ts
// Grant permission to other AWS account
declare const fn: lambda.Function;
const account = new iam.AccountPrincipal('123456789012');

fn.grantInvoke(account);
```

For more information, see
[Granting function access to other accounts](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html#permissions-resource-xaccountinvoke)
in the AWS Lambda Developer Guide.

### Grant function access to unowned principals

Providing an unowned principal (such as account principals, generic ARN
principals, service principals, and principals in other accounts) to a call to
`fn.grantInvoke` will result in a resource-based policy being created. If the
Expand All @@ -198,13 +247,6 @@ const servicePrincipalWithConditions = servicePrincipal.withConditions({
});

fn.grantInvoke(servicePrincipalWithConditions);

// Equivalent to:
fn.addPermission('my-service Invocation', {
principal: servicePrincipal,
sourceArn: sourceArn,
sourceAccount: sourceAccount,
});
```

## Versions
Expand Down
86 changes: 72 additions & 14 deletions packages/@aws-cdk/aws-lambda/lib/function-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,8 +343,10 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
return;
}

const principal = this.parsePermissionPrincipal(permission.principal);
const { sourceAccount, sourceArn } = this.parseConditions(permission.principal) ?? {};
let principal = this.parsePermissionPrincipal(permission.principal);

let { sourceArn, sourceAccount, principalOrgID } = this.validateConditionCombinations(permission.principal) ?? {};

const action = permission.action ?? 'lambda:InvokeFunction';
const scope = permission.scope ?? this;

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

const stringEquals = matchSingleKey('StringEquals', principal.policyFragment.conditions);
if (stringEquals) {
const orgId = matchSingleKey('aws:PrincipalOrgID', stringEquals);
if (orgId) {
// we will move the organization id to the `principalOrgId` property of `Permissions`.
return '*';
}
}

// Try a best-effort approach to support simple principals that are not any of the predefined
// classes, but are simple enough that they will fit into the Permission model. Main target
// here: imported Roles, Users, Groups.
Expand All @@ -584,17 +595,67 @@ export abstract class FunctionBase extends Resource implements IFunction, ec2.IC
}

throw new Error(`Invalid principal type for Lambda permission statement: ${principal.constructor.name}. ` +
'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal');
'Supported: AccountPrincipal, ArnPrincipal, ServicePrincipal, OrganizationPrincipal');

/**
* Returns the value at the key if the object contains the key and nothing else. Otherwise,
* returns undefined.
*/
function matchSingleKey(key: string, obj: Record<string, any>): any | undefined {
if (Object.keys(obj).length !== 1) { return undefined; }

return obj[key];
}

}

private parseConditions(principal: iam.IPrincipal): { sourceAccount: string, sourceArn: string } | null {
private validateConditionCombinations(principal: iam.IPrincipal): {
sourceArn: string | undefined,
sourceAccount: string | undefined,
principalOrgID: string | undefined,
} | undefined {
const conditions = this.validateConditions(principal);

if (!conditions) { return undefined; }

const sourceArn = conditions.ArnLike ? conditions.ArnLike['aws:SourceArn'] : undefined;
const sourceAccount = conditions.StringEquals ? conditions.StringEquals['aws:SourceAccount'] : undefined;
const principalOrgID = conditions.StringEquals ? conditions.StringEquals['aws:PrincipalOrgID'] : undefined;

// PrincipalOrgID cannot be combined with any other conditions
if (principalOrgID && (sourceArn || sourceAccount)) {
throw new Error('PrincipalWithConditions had unsupported condition combinations for Lambda permission statement: principalOrgID cannot be set with other conditions.');
}

return {
sourceArn,
sourceAccount,
principalOrgID,
};
}

private validateConditions(principal: iam.IPrincipal): iam.Conditions | undefined {
if (this.isPrincipalWithConditions(principal)) {
const conditions: iam.Conditions = principal.policyFragment.conditions;
const conditionPairs = flatMap(
Object.entries(conditions),
([operator, conditionObjs]) => Object.keys(conditionObjs as object).map(key => { return { operator, key }; }),
);
const supportedPrincipalConditions = [{ operator: 'ArnLike', key: 'aws:SourceArn' }, { operator: 'StringEquals', key: 'aws:SourceAccount' }];

// These are all the supported conditions. Some combinations are not supported,
// like only 'aws:SourceArn' or 'aws:PrincipalOrgID' and 'aws:SourceAccount'.
// These will be validated through `this.validateConditionCombinations`.
const supportedPrincipalConditions = [{
operator: 'ArnLike',
key: 'aws:SourceArn',
},
{
operator: 'StringEquals',
key: 'aws:SourceAccount',
}, {
operator: 'StringEquals',
key: 'aws:PrincipalOrgID',
}];

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

if (unsupportedConditions.length == 0) {
return {
sourceAccount: conditions.StringEquals['aws:SourceAccount'],
sourceArn: conditions.ArnLike['aws:SourceArn'],
};
return conditions;
} else {
throw new Error(`PrincipalWithConditions had unsupported conditions for Lambda permission statement: ${JSON.stringify(unsupportedConditions)}. ` +
`Supported operator/condition pairs: ${JSON.stringify(supportedPrincipalConditions)}`);
}
} else {
return null;
}

return undefined;
}

private isPrincipalWithConditions(principal: iam.IPrincipal): principal is iam.PrincipalWithConditions {
return 'conditions' in principal;
private isPrincipalWithConditions(principal: iam.IPrincipal): boolean {
return Object.keys(principal.policyFragment.conditions).length > 0;
}
}

Expand Down
30 changes: 23 additions & 7 deletions packages/@aws-cdk/aws-lambda/lib/permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,22 @@ export interface Permission {
* A unique token that must be supplied by the principal invoking the
* function.
*
* @default The caller would not need to present a token.
* @default - The caller would not need to present a token.
*/
readonly eventSourceToken?: string;

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

Expand Down Expand Up @@ -67,6 +70,19 @@ export interface Permission {
*/
readonly sourceArn?: string;

/**
* The organization you want to grant permissions to. Use this ONLY if you
* need to grant permissions to a subset of the organization. If you want to
* grant permissions to the entire organization, sending the organization principal
* through the `principal` property will suffice.
*
* You can use this property to ensure that all source principals are owned by
* a specific organization.
*
* @default - No organizationId
*/
readonly organizationId?: string;

/**
* The authType for the function URL that you are granting permissions for.
*
Expand Down
Loading

0 comments on commit 2566017

Please sign in to comment.