Skip to content

Commit

Permalink
feat(neptune): support IAM authentication
Browse files Browse the repository at this point in the history
fixes aws#13461
  • Loading branch information
christophgysin committed Mar 8, 2021
1 parent 1e9cf6b commit f87ffee
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 2 deletions.
18 changes: 18 additions & 0 deletions packages/@aws-cdk/aws-neptune/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,24 @@ attributes:
const writeAddress = cluster.clusterEndpoint.socketAddress; // "HOSTNAME:PORT"
```

## IAM Authentication

You can also authenticate to a database cluster using AWS Identity and Access Management (IAM) database authentication;
See <https://docs.aws.amazon.com/neptune/latest/userguide/iam-auth.html> for more information and a list of supported
versions and limitations.

The following example shows enabling IAM authentication for a database cluster and granting connection access to an IAM role.

```ts
const cluster = new rds.DatabaseCluster(stack, 'Cluster', {
vpc,
instanceType: neptune.InstanceType.R5_LARGE,
iamAuthentication: true, // Optional - will be automatically set if you call grantConnect().
});
const role = new Role(stack, 'DBRole', { assumedBy: new AccountPrincipal(stack.account) });
instance.grantConnect(role); // Grant the role connection access to the DB.
```

## Customizing parameters

Neptune allows configuring database behavior by supplying custom parameter groups. For more details, refer to the
Expand Down
44 changes: 43 additions & 1 deletion packages/@aws-cdk/aws-neptune/lib/cluster.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import { Duration, IResource, RemovalPolicy, Resource, Token } from '@aws-cdk/core';
import { Aws, Duration, IResource, Lazy, RemovalPolicy, Resource, Token } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Endpoint } from './endpoint';
import { InstanceType } from './instance';
Expand Down Expand Up @@ -119,6 +119,13 @@ export interface DatabaseClusterProps {
*/
readonly dbClusterName?: string;

/**
* Map AWS Identity and Access Management (IAM) accounts to database accounts
*
* @default - `false`
*/
readonly iamAuthentication?: boolean;

/**
* Base identifier for instances
*
Expand Down Expand Up @@ -233,6 +240,11 @@ export interface IDatabaseCluster extends IResource, ec2.IConnectable {
* @attribute ReadEndpoint
*/
readonly clusterReadEndpoint: Endpoint;

/**
* Grant the given identity connection access to the database.
*/
grantConnect(grantee: iam.IGrantable): iam.Grant;
}

/**
Expand Down Expand Up @@ -283,6 +295,7 @@ export abstract class DatabaseClusterBase extends Resource implements IDatabaseC
public readonly clusterIdentifier = attrs.clusterIdentifier;
public readonly clusterEndpoint = new Endpoint(attrs.clusterEndpointAddress, attrs.port);
public readonly clusterReadEndpoint = new Endpoint(attrs.readerEndpointAddress, attrs.port);
protected enableIamAuthentication = true;
}

return new Import(scope, id);
Expand All @@ -307,6 +320,30 @@ export abstract class DatabaseClusterBase extends Resource implements IDatabaseC
* The connections object to implement IConnectable
*/
public abstract readonly connections: ec2.Connections;

protected abstract enableIamAuthentication?: boolean;

public grantConnect(grantee: iam.IGrantable): iam.Grant {
if (this.enableIamAuthentication === false) {
throw new Error('Cannot grant connect when IAM authentication is disabled');
}

this.enableIamAuthentication = true;
return iam.Grant.addToPrincipal({
grantee,
actions: ['neptune-db:*'],
resourceArns: [
[
'arn',
Aws.PARTITION,
'neptune-db',
Aws.REGION,
Aws.ACCOUNT_ID,
`${this.clusterIdentifier}/*`,
].join(':'),
],
});
}
}

/**
Expand Down Expand Up @@ -359,6 +396,8 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu
*/
public readonly instanceEndpoints: Endpoint[] = [];

protected enableIamAuthentication?: boolean;

constructor(scope: Construct, id: string, props: DatabaseClusterProps) {
super(scope, id);

Expand Down Expand Up @@ -396,6 +435,8 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu

const deletionProtection = props.deletionProtection ?? (props.removalPolicy === RemovalPolicy.RETAIN ? true : undefined);

this.enableIamAuthentication = props.iamAuthentication;

// Create the Neptune cluster
const cluster = new CfnDBCluster(this, 'Resource', {
// Basic
Expand All @@ -407,6 +448,7 @@ export class DatabaseCluster extends DatabaseClusterBase implements IDatabaseClu
dbClusterParameterGroupName: props.clusterParameterGroup?.clusterParameterGroupName,
deletionProtection: deletionProtection,
associatedRoles: props.associatedRoles ? props.associatedRoles.map(role => ({ roleArn: role.roleArn })) : undefined,
iamAuthEnabled: Lazy.any({ produce: () => this.enableIamAuthentication }),
// Backup
backupRetentionPeriod: props.backupRetention?.toDays(),
preferredBackupWindow: props.preferredBackupWindow,
Expand Down
71 changes: 70 additions & 1 deletion packages/@aws-cdk/aws-neptune/test/cluster.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import '@aws-cdk/assert/jest';
import { ResourcePart } from '@aws-cdk/assert';
import { ABSENT, ResourcePart } from '@aws-cdk/assert';
import * as ec2 from '@aws-cdk/aws-ec2';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
Expand Down Expand Up @@ -431,6 +431,75 @@ describe('DatabaseCluster', () => {
PreferredMaintenanceWindow: '07:34-08:04',
});
});

test('iam authentication - off by default', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
new DatabaseCluster(stack, 'Cluster', {
vpc,
instanceType: InstanceType.R5_LARGE,
});

// THEN
expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', {
IamAuthEnabled: ABSENT,
});
});

test('createGrant - creates IAM policy and enables IAM auth', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
const cluster = new DatabaseCluster(stack, 'Cluster', {
vpc,
instanceType: InstanceType.R5_LARGE,
});
const role = new iam.Role(stack, 'DBRole', {
assumedBy: new iam.AccountPrincipal(stack.account),
});
cluster.grantConnect(role);

// THEN
expect(stack).toHaveResourceLike('AWS::Neptune::DBCluster', {
IamAuthEnabled: true,
});
expect(stack).toHaveResource('AWS::IAM::Policy', {
PolicyDocument: {
Statement: [{
Effect: 'Allow',
Action: 'neptune-db:*',
Resource: {
'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':neptune-db:', { Ref: 'AWS::Region' }, ':', { Ref: 'AWS::AccountId' }, ':', { Ref: 'ClusterEB0386A7' }, '/*']],
},
}],
Version: '2012-10-17',
},
});
});

test('createGrant - throws if IAM auth disabled', () => {
// GIVEN
const stack = testStack();
const vpc = new ec2.Vpc(stack, 'VPC');

// WHEN
const cluster = new DatabaseCluster(stack, 'Cluster', {
vpc,
instanceType: InstanceType.R5_LARGE,
iamAuthentication: false,
});
const role = new iam.Role(stack, 'DBRole', {
assumedBy: new iam.AccountPrincipal(stack.account),
});

// THEN
expect(() => { cluster.grantConnect(role); }).toThrow(/Cannot grant connect when IAM authentication is disabled/);
});
});

function testStack() {
Expand Down

0 comments on commit f87ffee

Please sign in to comment.