Skip to content

Commit

Permalink
feat: add geofence collection
Browse files Browse the repository at this point in the history
  • Loading branch information
mazyu36 committed Jun 30, 2024
1 parent afe69b8 commit 4b80527
Show file tree
Hide file tree
Showing 15 changed files with 797 additions and 0 deletions.
26 changes: 26 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,29 @@ declare const role: iam.Role;
const placeIndex = new location.PlaceIndex(this, 'PlaceIndex');
placeIndex.grantSearch(role);
```

## Geofence Collection

Geofence collection resources allow you to store and manage geofences—virtual boundaries on a map. You can evaluate locations against a geofence collection resource and get notifications when the location update crosses the boundary of any of the geofences in the geofence collection.

```ts
declare const key: kms.Key;

new location.GeofenceCollection(this, 'GeofenceCollection', {
geofenceCollectionName: 'MyGeofenceCollection', // optional, defaults to a generated name
kmsKey: key, // optional, defaults to use an AWS managed key
});
```

Use the `grant()` or `grantRead()` method to grant the given identity permissions to perform actions
on the geofence collection:

```ts
declare const role: iam.Role;

const geofenceCollection = new location.GeofenceCollection(this, 'GeofenceCollection', {
geofenceCollectionName: 'MyGeofenceCollection',
});

geofenceCollection.grantRead(role);
```
160 changes: 160 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/geofence-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import { ArnFormat, IResource, Lazy, Resource, Stack, Token } from 'aws-cdk-lib/core';
import { Construct } from 'constructs';
import { CfnGeofenceCollection } from 'aws-cdk-lib/aws-location';
import { generateUniqueId } from './util';

/**
* A Geofence Collection
*/
export interface IGeofenceCollection extends IResource {
/**
* The name of the geofence collection
*
* @attribute
*/
readonly geofenceCollectionName: string;

/**
* The Amazon Resource Name (ARN) of the geofence collection resource
*
* @attribute Arn, CollectionArn
*/
readonly geofenceCollectionArn: string;
}

/**
* Properties for a geofence collection
*/
export interface GeofenceCollectionProps {
/**
* A name for the geofence collection
*
* @default - A name is automatically generated
*/
readonly geofenceCollectionName?: string;

/**
* A description for the geofence collection
*
* @default - no description
*/
readonly description?: string;

/**
* The customer managed to encrypt your data.
*
* @default - Use an AWS managed key
* @see https://docs.aws.amazon.com/location/latest/developerguide/encryption-at-rest.html
*/
readonly kmsKey?: kms.IKey;
}

abstract class GeofenceCollectionBase extends Resource implements IGeofenceCollection {
public abstract readonly geofenceCollectionName: string;
public abstract readonly geofenceCollectionArn: string;

/**
* Grant the given principal identity permissions to perform the actions on this geofence collection.
*/
public grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant {
return iam.Grant.addToPrincipal({
grantee: grantee,
actions: actions,
resourceArns: [this.geofenceCollectionArn],
});
}

/**
* Grant the given identity permissions to read this geofence collection
*
* See https://docs.aws.amazon.com/location/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-read-only-geofences
*/
public grantRead(grantee: iam.IGrantable): iam.Grant {
return this.grant(grantee,
'geo:ListGeofences',
'geo:GetGeofence',
);
}
}

/**
* A Geofence Collection
*
* @see https://docs.aws.amazon.com/location/latest/developerguide/geofence-tracker-concepts.html#geofence-overview
*/
export class GeofenceCollection extends GeofenceCollectionBase {
/**
* Use an existing geofence collection by name
*/
public static fromGeofenceCollectionName(scope: Construct, id: string, geofenceCollectionName: string): IGeofenceCollection {
const geofenceCollectionArn = Stack.of(scope).formatArn({
service: 'geo',
resource: 'geofence-collection',
resourceName: geofenceCollectionName,
});

return GeofenceCollection.fromGeofenceCollectionArn(scope, id, geofenceCollectionArn);
}

/**
* Use an existing geofence collection by ARN
*/
public static fromGeofenceCollectionArn(scope: Construct, id: string, geofenceCollectionArn: string): IGeofenceCollection {
const parsedArn = Stack.of(scope).splitArn(geofenceCollectionArn, ArnFormat.SLASH_RESOURCE_NAME);

if (!parsedArn.resourceName) {
throw new Error(`Geofence Collection Arn ${geofenceCollectionArn} does not have a resource name.`);
}

class Import extends GeofenceCollectionBase {
public readonly geofenceCollectionName = parsedArn.resourceName!;
public readonly geofenceCollectionArn = geofenceCollectionArn;
}

return new Import(scope, id, {
account: parsedArn.account,
region: parsedArn.region,
});
}

public readonly geofenceCollectionName: string;

public readonly geofenceCollectionArn: string;

/**
* The timestamp for when the geofence collection resource was created in ISO 8601 forma
*
* @attribute
*/
public readonly geofenceCollectionCreateTime: string;

/**
* The timestamp for when the geofence collection resource was last updated in ISO 8601 format
*
* @attribute
*/
public readonly geofenceCollectionUpdateTime: string;

constructor(scope: Construct, id: string, props: GeofenceCollectionProps = {}) {
if (props.geofenceCollectionName && !Token.isUnresolved(props.geofenceCollectionName) && !/^[-.\w]{1,100}$/.test(props.geofenceCollectionName)) {
throw new Error(`Invalid geofence collection name. The geofence collection name must be between 1 and 100 characters and contain only alphanumeric characters, hyphens, periods and underscores. Received: ${props.geofenceCollectionName}`);
}

super(scope, id, {
physicalName: props.geofenceCollectionName ?? Lazy.string({ produce: () => generateUniqueId(this) }),
});

const geofenceCollection = new CfnGeofenceCollection(this, 'Resource', {
collectionName: this.physicalName,
description: props.description,
kmsKeyId: props.kmsKey?.keyArn,
});

this.geofenceCollectionName = geofenceCollection.ref;
this.geofenceCollectionArn = geofenceCollection.attrArn;
this.geofenceCollectionCreateTime = geofenceCollection.attrCreateTime;
this.geofenceCollectionUpdateTime = geofenceCollection.attrUpdateTime;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './geofence-collection';
export * from './place-index';

// AWS::Location CloudFormation Resources:
9 changes: 9 additions & 0 deletions packages/@aws-cdk/aws-location-alpha/lib/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Names } from 'aws-cdk-lib/core';

export function generateUniqueId(context: any): string {
const name = Names.uniqueId(context);
if (name.length > 100) {
return name.substring(0, 50) + name.substring(name.length - 50);
}
return name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as location from '@aws-cdk/aws-location-alpha';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';

class Fixture extends Stack {
constructor(scope: Construct, id: string) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Match, Template } from 'aws-cdk-lib/assertions';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as kms from 'aws-cdk-lib/aws-kms';
import { Stack } from 'aws-cdk-lib';
import { GeofenceCollection } from '../lib/geofence-collection';

let stack: Stack;
beforeEach(() => {
stack = new Stack();
});

test('create a geofence collecction', () => {
new GeofenceCollection(stack, 'GeofenceCollection', { description: 'test' });

Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', {
CollectionName: 'GeofenceCollection',
Description: 'test',
});
});

test('throws with invalid name', () => {
expect(() => new GeofenceCollection(stack, 'GeofenceCollection', {
geofenceCollectionName: 'inv@lid',
})).toThrow('Invalid geofence collection name. The geofence collection name must be between 1 and 100 characters and contain only alphanumeric characters, hyphens, periods and underscores. Received: inv@lid');
});

test('grant read actions', () => {
const geofenceCollection = new GeofenceCollection(stack, 'GeofenceCollection', {
});

const role = new iam.Role(stack, 'Role', {
assumedBy: new iam.ServicePrincipal('foo'),
});

geofenceCollection.grantRead(role);

Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', Match.objectLike({
PolicyDocument: Match.objectLike({
Statement: [
{
Action: [
'geo:ListGeofences',
'geo:GetGeofence',
],
Effect: 'Allow',
Resource: {
'Fn::GetAtt': [
'GeofenceCollection6FAC681F',
'Arn',
],
},
},
],
}),
}));
});

test('import from arn', () => {
const geofenceCollectionArn = stack.formatArn({
service: 'geo',
resource: 'geofence-collection',
resourceName: 'MyGeofenceCollection',
});
const geofenceCollection = GeofenceCollection.fromGeofenceCollectionArn(stack, 'GeofenceCollection', geofenceCollectionArn);

// THEN
expect(geofenceCollection.geofenceCollectionName).toEqual('MyGeofenceCollection');
expect(geofenceCollection.geofenceCollectionArn).toEqual(geofenceCollectionArn);
});

test('import from name', () => {
// WHEN
const geofenceCollectionName = 'MyGeofenceCollection';
const geofenceCollection = GeofenceCollection.fromGeofenceCollectionName(stack, 'GeofenceCollection', geofenceCollectionName);

// THEN
expect(geofenceCollection.geofenceCollectionName).toEqual(geofenceCollectionName);
expect(geofenceCollection.geofenceCollectionArn).toEqual(stack.formatArn({
service: 'geo',
resource: 'geofence-collection',
resourceName: 'MyGeofenceCollection',
}));
});

test('create a geofence collection with a customer managed key)', () => {
// GIVEN
const kmsKey = new kms.Key(stack, 'Key');

// WHEN
new GeofenceCollection(stack, 'GeofenceCollection',
{ kmsKey },
);

// THEN
Template.fromStack(stack).hasResourceProperties('AWS::Location::GeofenceCollection', {
KmsKeyId: stack.resolve(kmsKey.keyArn),
});
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4b80527

Please sign in to comment.