|
| 1 | +import { ArnFormat, Aws, IResource, Lazy, Resource, Stack, Token, UnscopedValidationError, ValidationError } from 'aws-cdk-lib/core'; |
| 2 | +import { Construct } from 'constructs'; |
| 3 | +import { CfnAPIKey } from 'aws-cdk-lib/aws-location'; |
| 4 | +import { generateUniqueId } from './util'; |
| 5 | + |
| 6 | +/** |
| 7 | + * An API Key |
| 8 | + */ |
| 9 | +export interface IApiKey extends IResource { |
| 10 | + /** |
| 11 | + * The name of the api key |
| 12 | + * |
| 13 | + * @attribute |
| 14 | + */ |
| 15 | + readonly apiKeyName: string; |
| 16 | + |
| 17 | + /** |
| 18 | + * The Amazon Resource Name (ARN) of the api key resource |
| 19 | + * |
| 20 | + * @attribute Arn, apiKeyArn |
| 21 | + */ |
| 22 | + readonly apiKeyArn: string; |
| 23 | +} |
| 24 | + |
| 25 | +/** |
| 26 | + * Properties for an API Key |
| 27 | + */ |
| 28 | +export interface ApiKeyProps { |
| 29 | + /** |
| 30 | + * A name for the api key |
| 31 | + * |
| 32 | + * Must be between 1 and 100 characters and contain only alphanumeric characters, |
| 33 | + * hyphens, periods and underscores. |
| 34 | + * |
| 35 | + * Must be a unique API key name. |
| 36 | + * |
| 37 | + * @default - A name is automatically generated |
| 38 | + */ |
| 39 | + readonly apiKeyName?: string; |
| 40 | + |
| 41 | + /** |
| 42 | + * A description for the api key |
| 43 | + * |
| 44 | + * @default - no description |
| 45 | + */ |
| 46 | + readonly description?: string; |
| 47 | + |
| 48 | + /** |
| 49 | + * The optional timestamp for when the API key resource will expire |
| 50 | + * |
| 51 | + * `expireTime` must be set when `noExpiry` is false or undefined. |
| 52 | + * When `expireTime` is not set, `noExpiry` must be `true`. |
| 53 | + * |
| 54 | + * @default undefined - The API Key never expires |
| 55 | + */ |
| 56 | + readonly expireTime?: Date; |
| 57 | + |
| 58 | + /** |
| 59 | + * `forceDelete` bypasses an API key's expiry conditions and deletes the key. |
| 60 | + * Set the parameter true to delete the key or to false to not preemptively delete the API key. |
| 61 | + * |
| 62 | + * @default undefined - not force delete |
| 63 | + */ |
| 64 | + readonly forceDelete?: boolean; |
| 65 | + |
| 66 | + /** |
| 67 | + * The boolean flag to be included for updating ExpireTime or Restrictions details. |
| 68 | + * Must be set to true to update an API key resource that has been used in the past 7 days. |
| 69 | + * False if force update is not preferred. |
| 70 | + * |
| 71 | + * @default undefined - not force update |
| 72 | + */ |
| 73 | + readonly forceUpdate?: boolean; |
| 74 | + |
| 75 | + /** |
| 76 | + * Whether the API key should expire. |
| 77 | + * |
| 78 | + * Set to `true` when `expireTime` is not set. |
| 79 | + * When you set `expireTime`, `noExpiry` must be `false` or `undefined`. |
| 80 | + * |
| 81 | + * @default undefined - The API Key expires at `expireTime` |
| 82 | + */ |
| 83 | + readonly noExpiry?: boolean; |
| 84 | + |
| 85 | + /** |
| 86 | + * A list of allowed actions for Maps that an API key resource grants permissions to perform. |
| 87 | + * |
| 88 | + * @default - no actions for Maps are permitted |
| 89 | + */ |
| 90 | + readonly allowMapsActions?: AllowMapsAction[]; |
| 91 | + |
| 92 | + /** |
| 93 | + * A list of allowed actions for Places that an API key resource grants permissions to perform. |
| 94 | + * |
| 95 | + * @default - no actions for Places are permitted |
| 96 | + */ |
| 97 | + readonly allowPlacesActions?: AllowPlacesAction[]; |
| 98 | + |
| 99 | + /** |
| 100 | + * A list of allowed actions for Routes that an API key resource grants permissions to perform. |
| 101 | + * |
| 102 | + * @default - no actions for Routes are permitted |
| 103 | + */ |
| 104 | + readonly allowRoutesActions?: AllowRoutesAction[]; |
| 105 | + |
| 106 | + /** |
| 107 | + * An optional list of allowed HTTP referers for which requests must originate from. |
| 108 | + * Requests using this API key from other domains will not be allowed. |
| 109 | + * |
| 110 | + * @see https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-location-apikey-apikeyrestrictions.html#cfn-location-apikey-apikeyrestrictions-allowreferers |
| 111 | + * @default - no Referer |
| 112 | + */ |
| 113 | + readonly allowReferers?: string[]; |
| 114 | +} |
| 115 | + |
| 116 | +/** |
| 117 | + * Actions for Maps that an API key resource grants permissions to perform. |
| 118 | + * |
| 119 | + * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonlocationservicemaps.html |
| 120 | + */ |
| 121 | +export enum AllowMapsAction { |
| 122 | + /** |
| 123 | + * Allows getting static map images. |
| 124 | + */ |
| 125 | + GET_STATIC_MAP = 'geo-maps:GetStaticMap', |
| 126 | + |
| 127 | + /** |
| 128 | + * Allows getting map tiles for rendering. |
| 129 | + */ |
| 130 | + GET_TILE = 'geo-maps:GetTile', |
| 131 | + |
| 132 | + /** |
| 133 | + * Allows any maps actions. |
| 134 | + */ |
| 135 | + ANY = 'geo-maps:*', |
| 136 | +} |
| 137 | + |
| 138 | +/** |
| 139 | + * Actions for Places that an API key resource grants permissions to perform. |
| 140 | + * |
| 141 | + * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonlocationserviceplaces.html |
| 142 | + */ |
| 143 | +export enum AllowPlacesAction { |
| 144 | + /** |
| 145 | + * Allows auto-completion of search text. |
| 146 | + */ |
| 147 | + AUTOCOMPLETE = 'geo-places:Autocomplete', |
| 148 | + |
| 149 | + /** |
| 150 | + * Allows finding geo coordinates of a known place. |
| 151 | + */ |
| 152 | + GEOCODE = 'geo-places:Geocode', |
| 153 | + |
| 154 | + /** |
| 155 | + * Allows getting details of a place. |
| 156 | + */ |
| 157 | + GET_PLACE = 'geo-places:GetPlace', |
| 158 | + |
| 159 | + /** |
| 160 | + * Allows getting nearest address to geo coordinates. |
| 161 | + */ |
| 162 | + REVERSE_GEOCODE = 'geo-places:ReverseGeocode', |
| 163 | + |
| 164 | + /** |
| 165 | + * Allows category based places search around geo coordinates. |
| 166 | + */ |
| 167 | + SEARCH_NEARBY = 'geo-places:SearchNearby', |
| 168 | + |
| 169 | + /** |
| 170 | + * Allows place or address search based on free-form text. |
| 171 | + */ |
| 172 | + SEARCH_TEXT = 'geo-places:SearchText', |
| 173 | + |
| 174 | + /** |
| 175 | + * Allows suggestions based on an incomplete or misspelled query. |
| 176 | + */ |
| 177 | + SUGGEST = 'geo-places:Suggest', |
| 178 | + |
| 179 | + /** |
| 180 | + * Allows any places actions. |
| 181 | + */ |
| 182 | + ANY = 'geo-places:*', |
| 183 | +} |
| 184 | + |
| 185 | +/** |
| 186 | + * Actions for Routes that an API key resource grants permissions to perform. |
| 187 | + * |
| 188 | + * @see https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonlocationserviceroutes.html |
| 189 | + */ |
| 190 | +export enum AllowRoutesAction { |
| 191 | + /** |
| 192 | + * Allows isoline calculation. |
| 193 | + */ |
| 194 | + CALCULATE_ISOLINES = 'geo-routes:CalculateIsolines', |
| 195 | + |
| 196 | + /** |
| 197 | + * Allows point to point routing. |
| 198 | + */ |
| 199 | + CALCULATE_ROUTES = 'geo-routes:CalculateRoutes', |
| 200 | + |
| 201 | + /** |
| 202 | + * Allows matrix routing. |
| 203 | + */ |
| 204 | + CALCULATE_ROUTE_MATRIX = 'geo-routes:CalculateRouteMatrix', |
| 205 | + |
| 206 | + /** |
| 207 | + * Allows computing the best sequence of waypoints. |
| 208 | + */ |
| 209 | + OPTIMIZE_WAYPOINTS = 'geo-routes:OptimizeWaypoints', |
| 210 | + |
| 211 | + /** |
| 212 | + * Allows snapping GPS points to a likely route. |
| 213 | + */ |
| 214 | + SNAP_TO_ROADS = 'geo-routes:SnapToRoads', |
| 215 | + |
| 216 | + /** |
| 217 | + * Allows any routes actions. |
| 218 | + */ |
| 219 | + ANY = 'geo-routes:*', |
| 220 | +} |
| 221 | + |
| 222 | +/** |
| 223 | + * An API Key |
| 224 | + * |
| 225 | + * @see https://docs.aws.amazon.com/location/latest/developerguide/using-apikeys.html |
| 226 | + */ |
| 227 | +export class ApiKey extends Resource implements IApiKey { |
| 228 | + /** |
| 229 | + * Use an existing api key by name |
| 230 | + */ |
| 231 | + public static fromApiKeyName(scope: Construct, id: string, apiKeyName: string): IApiKey { |
| 232 | + const apiKeyArn = Stack.of(scope).formatArn({ |
| 233 | + service: 'geo', |
| 234 | + resource: 'api-key', |
| 235 | + resourceName: apiKeyName, |
| 236 | + }); |
| 237 | + |
| 238 | + return ApiKey.fromApiKeyArn(scope, id, apiKeyArn); |
| 239 | + } |
| 240 | + |
| 241 | + /** |
| 242 | + * Use an existing api key by ARN |
| 243 | + */ |
| 244 | + public static fromApiKeyArn(scope: Construct, id: string, apiKeyArn: string): IApiKey { |
| 245 | + const parsedArn = Stack.of(scope).splitArn(apiKeyArn, ArnFormat.SLASH_RESOURCE_NAME); |
| 246 | + |
| 247 | + if (!parsedArn.resourceName) { |
| 248 | + throw new UnscopedValidationError(`API Key Arn ${apiKeyArn} does not have a resource name.`); |
| 249 | + } |
| 250 | + |
| 251 | + class Import extends Resource implements IApiKey { |
| 252 | + public readonly apiKeyName = parsedArn.resourceName!; |
| 253 | + public readonly apiKeyArn = apiKeyArn; |
| 254 | + } |
| 255 | + |
| 256 | + return new Import(scope, id, { |
| 257 | + account: parsedArn.account, |
| 258 | + region: parsedArn.region, |
| 259 | + }); |
| 260 | + } |
| 261 | + |
| 262 | + public readonly apiKeyName: string; |
| 263 | + |
| 264 | + public readonly apiKeyArn: string; |
| 265 | + |
| 266 | + /** |
| 267 | + * The timestamp for when the api key resource was created in ISO 8601 format |
| 268 | + * |
| 269 | + * @attribute |
| 270 | + */ |
| 271 | + public readonly apiKeyCreateTime: string; |
| 272 | + |
| 273 | + /** |
| 274 | + * The timestamp for when the api key resource was last updated in ISO 8601 format |
| 275 | + * |
| 276 | + * @attribute |
| 277 | + */ |
| 278 | + public readonly apiKeyUpdateTime: string; |
| 279 | + |
| 280 | + constructor(scope: Construct, id: string, props: ApiKeyProps = {}) { |
| 281 | + super(scope, id, { |
| 282 | + physicalName: props.apiKeyName ?? Lazy.string({ produce: () => generateUniqueId(this) }), |
| 283 | + }); |
| 284 | + |
| 285 | + if (props.description && !Token.isUnresolved(props.description) && props.description.length > 1000) { |
| 286 | + throw new ValidationError(`\`description\` must be between 0 and 1000 characters. Received: ${props.description.length} characters`, this); |
| 287 | + } |
| 288 | + |
| 289 | + if (props.apiKeyName !== undefined && !Token.isUnresolved(props.apiKeyName)) { |
| 290 | + if (props.apiKeyName.length < 1 || props.apiKeyName.length > 100) { |
| 291 | + throw new ValidationError(`\`apiKeyName\` must be between 1 and 100 characters, got: ${props.apiKeyName.length} characters.`, this); |
| 292 | + } |
| 293 | + |
| 294 | + if (!/^[-._\w]+$/.test(props.apiKeyName)) { |
| 295 | + throw new ValidationError(`\`apiKeyName\` must contain only alphanumeric characters, hyphens, periods and underscores, got: ${props.apiKeyName}.`, this); |
| 296 | + } |
| 297 | + } |
| 298 | + |
| 299 | + if (props.expireTime !== undefined && props.noExpiry === true) { |
| 300 | + throw new ValidationError('`expireTime` must not be set when `noExpiry` has value true.', this); |
| 301 | + } |
| 302 | + |
| 303 | + if (props.expireTime === undefined && props.noExpiry !== true) { |
| 304 | + throw new ValidationError('`expireTime` must be set when `noExpiry` is false or undefined.', this); |
| 305 | + } |
| 306 | + |
| 307 | + if (!props.allowMapsActions && !props.allowPlacesActions && !props.allowRoutesActions) { |
| 308 | + throw new ValidationError('At least one of `allowMapsActions`, `allowPlacesActions`, or `allowRoutesActions` must be specified.', this); |
| 309 | + } |
| 310 | + |
| 311 | + if (props.allowReferers !== undefined && |
| 312 | + (props.allowReferers.length < 1 || props.allowReferers.length > 5)) { |
| 313 | + throw new ValidationError(`\`allowReferers\` must be between 1 and 5 elements, got: ${props.allowReferers.length} elements.`, this); |
| 314 | + } |
| 315 | + |
| 316 | + const apiKey = new CfnAPIKey(this, 'Resource', { |
| 317 | + keyName: this.physicalName, |
| 318 | + description: props.description, |
| 319 | + expireTime: props.expireTime?.toISOString(), |
| 320 | + forceDelete: props.forceDelete, |
| 321 | + forceUpdate: props.forceUpdate, |
| 322 | + noExpiry: props.noExpiry, |
| 323 | + restrictions: this._renderRestrictions(props), |
| 324 | + }); |
| 325 | + |
| 326 | + this.apiKeyName = apiKey.ref; |
| 327 | + this.apiKeyArn = apiKey.attrArn; |
| 328 | + this.apiKeyCreateTime = apiKey.attrCreateTime; |
| 329 | + this.apiKeyUpdateTime = apiKey.attrUpdateTime; |
| 330 | + } |
| 331 | + |
| 332 | + /** |
| 333 | + * Renders the restrictions property for API Keys |
| 334 | + * |
| 335 | + * NOTE: added allowResources are AWS managed resources. |
| 336 | + */ |
| 337 | + private _renderRestrictions(props: ApiKeyProps): CfnAPIKey.ApiKeyRestrictionsProperty { |
| 338 | + let allowResources = []; |
| 339 | + if (props.allowMapsActions) { |
| 340 | + allowResources.push(`arn:${Aws.PARTITION}:geo-maps:${Stack.of(this).region}::provider/default`); |
| 341 | + } |
| 342 | + if (props.allowPlacesActions) { |
| 343 | + allowResources.push(`arn:${Aws.PARTITION}:geo-places:${Stack.of(this).region}::provider/default`); |
| 344 | + } |
| 345 | + if (props.allowRoutesActions) { |
| 346 | + allowResources.push(`arn:${Aws.PARTITION}:geo-routes:${Stack.of(this).region}::provider/default`); |
| 347 | + } |
| 348 | + |
| 349 | + return { |
| 350 | + allowActions: [ |
| 351 | + ...props.allowMapsActions ?? [], |
| 352 | + ...props.allowPlacesActions ?? [], |
| 353 | + ...props.allowRoutesActions ?? [], |
| 354 | + ].map((action) => action.toString()), |
| 355 | + allowReferers: props.allowReferers, |
| 356 | + allowResources, |
| 357 | + }; |
| 358 | + } |
| 359 | +} |
0 commit comments