1 | # AWS Identity and Access Management Construct Library
|
2 |
|
3 |
|
4 | ---
|
5 |
|
6 | ![cfn-resources: Stable](https://img.shields.io/badge/cfn--resources-stable-success.svg?style=for-the-badge)
|
7 |
|
8 | ![cdk-constructs: Stable](https://img.shields.io/badge/cdk--constructs-stable-success.svg?style=for-the-badge)
|
9 |
|
10 | ---
|
11 |
|
12 |
|
13 |
|
14 | Define a role and add permissions to it. This will automatically create and
|
15 | attach an IAM policy to the role:
|
16 |
|
17 | [attaching permissions to role](test/example.role.lit.ts)
|
18 |
|
19 | Define a policy and attach it to groups, users and roles. Note that it is possible to attach
|
20 | the policy either by calling `xxx.attachInlinePolicy(policy)` or `policy.attachToXxx(xxx)`.
|
21 |
|
22 | [attaching policies to user and group](test/example.attaching.lit.ts)
|
23 |
|
24 | Managed policies can be attached using `xxx.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName(policyName))`:
|
25 |
|
26 | [attaching managed policies](test/example.managedpolicy.lit.ts)
|
27 |
|
28 | ## Granting permissions to resources
|
29 |
|
30 | Many of the AWS CDK resources have `grant*` methods that allow you to grant other resources access to that resource. As an example, the following code gives a Lambda function write permissions (Put, Update, Delete) to a DynamoDB table.
|
31 |
|
32 | ```ts
|
33 | declare const fn: lambda.Function;
|
34 | declare const table: dynamodb.Table;
|
35 |
|
36 | table.grantWriteData(fn);
|
37 | ```
|
38 |
|
39 | The more generic `grant` method allows you to give specific permissions to a resource:
|
40 |
|
41 | ```ts
|
42 | declare const fn: lambda.Function;
|
43 | declare const table: dynamodb.Table;
|
44 |
|
45 | table.grant(fn, 'dynamodb:PutItem');
|
46 | ```
|
47 |
|
48 | The `grant*` methods accept an `IGrantable` object. This interface is implemented by IAM principlal resources (groups, users and roles) and resources that assume a role such as a Lambda function, EC2 instance or a Codebuild project.
|
49 |
|
50 | You can find which `grant*` methods exist for a resource in the [AWS CDK API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html).
|
51 |
|
52 | ## Roles
|
53 |
|
54 | Many AWS resources require *Roles* to operate. These Roles define the AWS API
|
55 | calls an instance or other AWS service is allowed to make.
|
56 |
|
57 | Creating Roles and populating them with the right permissions *Statements* is
|
58 | a necessary but tedious part of setting up AWS infrastructure. In order to
|
59 | help you focus on your business logic, CDK will take care of creating
|
60 | roles and populating them with least-privilege permissions automatically.
|
61 |
|
62 | All constructs that require Roles will create one for you if don't specify
|
63 | one at construction time. Permissions will be added to that role
|
64 | automatically if you associate the construct with other constructs from the
|
65 | AWS Construct Library (for example, if you tell an *AWS CodePipeline* to trigger
|
66 | an *AWS Lambda Function*, the Pipeline's Role will automatically get
|
67 | `lambda:InvokeFunction` permissions on that particular Lambda Function),
|
68 | or if you explicitly grant permissions using `grant` functions (see the
|
69 | previous section).
|
70 |
|
71 | ### Opting out of automatic permissions management
|
72 |
|
73 | You may prefer to manage a Role's permissions yourself instead of having the
|
74 | CDK automatically manage them for you. This may happen in one of the
|
75 | following cases:
|
76 |
|
77 | * You don't like the permissions that CDK automatically generates and
|
78 | want to substitute your own set.
|
79 | * The least-permissions policy that the CDK generates is becoming too
|
80 | big for IAM to store, and you need to add some wildcards to keep the
|
81 | policy size down.
|
82 |
|
83 | To prevent constructs from updating your Role's policy, pass the object
|
84 | returned by `myRole.withoutPolicyUpdates()` instead of `myRole` itself.
|
85 |
|
86 | For example, to have an AWS CodePipeline *not* automatically add the required
|
87 | permissions to trigger the expected targets, do the following:
|
88 |
|
89 | ```ts
|
90 | const role = new iam.Role(this, 'Role', {
|
91 | assumedBy: new iam.ServicePrincipal('codepipeline.amazonaws.com'),
|
92 | // custom description if desired
|
93 | description: 'This is a custom role...',
|
94 | });
|
95 |
|
96 | new codepipeline.Pipeline(this, 'Pipeline', {
|
97 | // Give the Pipeline an immutable view of the Role
|
98 | role: role.withoutPolicyUpdates(),
|
99 | });
|
100 |
|
101 | // You now have to manage the Role policies yourself
|
102 | role.addToPolicy(new iam.PolicyStatement({
|
103 | actions: [/* whatever actions you want */],
|
104 | resources: [/* whatever resources you intend to touch */],
|
105 | }));
|
106 | ```
|
107 |
|
108 | ### Using existing roles
|
109 |
|
110 | If there are Roles in your account that have already been created which you
|
111 | would like to use in your CDK application, you can use `Role.fromRoleArn` to
|
112 | import them, as follows:
|
113 |
|
114 | ```ts
|
115 | const role = iam.Role.fromRoleArn(this, 'Role', 'arn:aws:iam::123456789012:role/MyExistingRole', {
|
116 | // Set 'mutable' to 'false' to use the role as-is and prevent adding new
|
117 | // policies to it. The default is 'true', which means the role may be
|
118 | // modified as part of the deployment.
|
119 | mutable: false,
|
120 | });
|
121 | ```
|
122 |
|
123 | ## Configuring an ExternalId
|
124 |
|
125 | If you need to create Roles that will be assumed by third parties, it is generally a good idea to [require an `ExternalId`
|
126 | to assume them](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html). Configuring
|
127 | an `ExternalId` works like this:
|
128 |
|
129 | [supplying an external ID](test/example.external-id.lit.ts)
|
130 |
|
131 | ## Principals vs Identities
|
132 |
|
133 | When we say *Principal*, we mean an entity you grant permissions to. This
|
134 | entity can be an AWS Service, a Role, or something more abstract such as "all
|
135 | users in this account" or even "all users in this organization". An
|
136 | *Identity* is an IAM representing a single IAM entity that can have
|
137 | a policy attached, one of `Role`, `User`, or `Group`.
|
138 |
|
139 | ## IAM Principals
|
140 |
|
141 | When defining policy statements as part of an AssumeRole policy or as part of a
|
142 | resource policy, statements would usually refer to a specific IAM principal
|
143 | under `Principal`.
|
144 |
|
145 | IAM principals are modeled as classes that derive from the `iam.PolicyPrincipal`
|
146 | abstract class. Principal objects include principal type (string) and value
|
147 | (array of string), optional set of conditions and the action that this principal
|
148 | requires when it is used in an assume role policy document.
|
149 |
|
150 | To add a principal to a policy statement you can either use the abstract
|
151 | `statement.addPrincipal`, one of the concrete `addXxxPrincipal` methods:
|
152 |
|
153 | * `addAwsPrincipal`, `addArnPrincipal` or `new ArnPrincipal(arn)` for `{ "AWS": arn }`
|
154 | * `addAwsAccountPrincipal` or `new AccountPrincipal(accountId)` for `{ "AWS": account-arn }`
|
155 | * `addServicePrincipal` or `new ServicePrincipal(service)` for `{ "Service": service }`
|
156 | * `addAccountRootPrincipal` or `new AccountRootPrincipal()` for `{ "AWS": { "Ref: "AWS::AccountId" } }`
|
157 | * `addCanonicalUserPrincipal` or `new CanonicalUserPrincipal(id)` for `{ "CanonicalUser": id }`
|
158 | * `addFederatedPrincipal` or `new FederatedPrincipal(federated, conditions, assumeAction)` for
|
159 | `{ "Federated": arn }` and a set of optional conditions and the assume role action to use.
|
160 | * `addAnyPrincipal` or `new AnyPrincipal` for `{ "AWS": "*" }`
|
161 |
|
162 | If multiple principals are added to the policy statement, they will be merged together:
|
163 |
|
164 | ```ts
|
165 | const statement = new iam.PolicyStatement();
|
166 | statement.addServicePrincipal('cloudwatch.amazonaws.com');
|
167 | statement.addServicePrincipal('ec2.amazonaws.com');
|
168 | statement.addArnPrincipal('arn:aws:boom:boom');
|
169 | ```
|
170 |
|
171 | Will result in:
|
172 |
|
173 | ```json
|
174 | {
|
175 | "Principal": {
|
176 | "Service": [ "cloudwatch.amazonaws.com", "ec2.amazonaws.com" ],
|
177 | "AWS": "arn:aws:boom:boom"
|
178 | }
|
179 | }
|
180 | ```
|
181 |
|
182 | The `CompositePrincipal` class can also be used to define complex principals, for example:
|
183 |
|
184 | ```ts
|
185 | const role = new iam.Role(this, 'MyRole', {
|
186 | assumedBy: new iam.CompositePrincipal(
|
187 | new iam.ServicePrincipal('ec2.amazonaws.com'),
|
188 | new iam.AccountPrincipal('1818188181818187272')
|
189 | ),
|
190 | });
|
191 | ```
|
192 |
|
193 | The `PrincipalWithConditions` class can be used to add conditions to a
|
194 | principal, especially those that don't take a `conditions` parameter in their
|
195 | constructor. The `principal.withConditions()` method can be used to create a
|
196 | `PrincipalWithConditions` from an existing principal, for example:
|
197 |
|
198 | ```ts
|
199 | const principal = new iam.AccountPrincipal('123456789000')
|
200 | .withConditions({ StringEquals: { foo: "baz" } });
|
201 | ```
|
202 |
|
203 | > NOTE: If you need to define an IAM condition that uses a token (such as a
|
204 | > deploy-time attribute of another resource) in a JSON map key, use `CfnJson` to
|
205 | > render this condition. See [this test](./test/integ.condition-with-ref.ts) for
|
206 | > an example.
|
207 |
|
208 | The `WebIdentityPrincipal` class can be used as a principal for web identities like
|
209 | Cognito, Amazon, Google or Facebook, for example:
|
210 |
|
211 | ```ts
|
212 | const principal = new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com', {
|
213 | 'StringEquals': { 'cognito-identity.amazonaws.com:aud': 'us-east-2:12345678-abcd-abcd-abcd-123456' },
|
214 | 'ForAnyValue:StringLike': {'cognito-identity.amazonaws.com:amr': 'unauthenticated' },
|
215 | });
|
216 | ```
|
217 |
|
218 | If your identity provider is configured to assume a Role with [session
|
219 | tags](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html), you
|
220 | need to call `.withSessionTags()` to add the required permissions to the Role's
|
221 | policy document:
|
222 |
|
223 | ```ts
|
224 | new iam.Role(this, 'Role', {
|
225 | assumedBy: new iam.WebIdentityPrincipal('cognito-identity.amazonaws.com', {
|
226 | 'StringEquals': {
|
227 | 'cognito-identity.amazonaws.com:aud': 'us-east-2:12345678-abcd-abcd-abcd-123456',
|
228 | },
|
229 | 'ForAnyValue:StringLike': {
|
230 | 'cognito-identity.amazonaws.com:amr': 'unauthenticated',
|
231 | },
|
232 | }).withSessionTags(),
|
233 | });
|
234 | ```
|
235 |
|
236 |
|
237 | ## Parsing JSON Policy Documents
|
238 |
|
239 | The `PolicyDocument.fromJson` and `PolicyStatement.fromJson` static methods can be used to parse JSON objects. For example:
|
240 |
|
241 | ```ts
|
242 | const policyDocument = {
|
243 | "Version": "2012-10-17",
|
244 | "Statement": [
|
245 | {
|
246 | "Sid": "FirstStatement",
|
247 | "Effect": "Allow",
|
248 | "Action": ["iam:ChangePassword"],
|
249 | "Resource": "*"
|
250 | },
|
251 | {
|
252 | "Sid": "SecondStatement",
|
253 | "Effect": "Allow",
|
254 | "Action": "s3:ListAllMyBuckets",
|
255 | "Resource": "*"
|
256 | },
|
257 | {
|
258 | "Sid": "ThirdStatement",
|
259 | "Effect": "Allow",
|
260 | "Action": [
|
261 | "s3:List*",
|
262 | "s3:Get*"
|
263 | ],
|
264 | "Resource": [
|
265 | "arn:aws:s3:::confidential-data",
|
266 | "arn:aws:s3:::confidential-data/*"
|
267 | ],
|
268 | "Condition": {"Bool": {"aws:MultiFactorAuthPresent": "true"}}
|
269 | }
|
270 | ]
|
271 | };
|
272 |
|
273 | const customPolicyDocument = iam.PolicyDocument.fromJson(policyDocument);
|
274 |
|
275 | // You can pass this document as an initial document to a ManagedPolicy
|
276 | // or inline Policy.
|
277 | const newManagedPolicy = new iam.ManagedPolicy(this, 'MyNewManagedPolicy', {
|
278 | document: customPolicyDocument,
|
279 | });
|
280 | const newPolicy = new iam.Policy(this, 'MyNewPolicy', {
|
281 | document: customPolicyDocument,
|
282 | });
|
283 | ```
|
284 |
|
285 | ## Permissions Boundaries
|
286 |
|
287 | [Permissions
|
288 | Boundaries](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html)
|
289 | can be used as a mechanism to prevent privilege esclation by creating new
|
290 | `Role`s. Permissions Boundaries are a Managed Policy, attached to Roles or
|
291 | Users, that represent the *maximum* set of permissions they can have. The
|
292 | effective set of permissions of a Role (or User) will be the intersection of
|
293 | the Identity Policy and the Permissions Boundary attached to the Role (or
|
294 | User). Permissions Boundaries are typically created by account
|
295 | Administrators, and their use on newly created `Role`s will be enforced by
|
296 | IAM policies.
|
297 |
|
298 | It is possible to attach Permissions Boundaries to all Roles created in a construct
|
299 | tree all at once:
|
300 |
|
301 | ```ts
|
302 | // This imports an existing policy.
|
303 | const boundary = iam.ManagedPolicy.fromManagedPolicyArn(this, 'Boundary', 'arn:aws:iam::123456789012:policy/boundary');
|
304 |
|
305 | // This creates a new boundary
|
306 | const boundary2 = new iam.ManagedPolicy(this, 'Boundary2', {
|
307 | statements: [
|
308 | new iam.PolicyStatement({
|
309 | effect: iam.Effect.DENY,
|
310 | actions: ['iam:*'],
|
311 | resources: ['*'],
|
312 | }),
|
313 | ],
|
314 | });
|
315 |
|
316 | // Directly apply the boundary to a Role you create
|
317 | declare const role: iam.Role;
|
318 | iam.PermissionsBoundary.of(role).apply(boundary);
|
319 |
|
320 | // Apply the boundary to an Role that was implicitly created for you
|
321 | declare const fn: lambda.Function;
|
322 | iam.PermissionsBoundary.of(fn).apply(boundary);
|
323 |
|
324 | // Apply the boundary to all Roles in a stack
|
325 | iam.PermissionsBoundary.of(this).apply(boundary);
|
326 |
|
327 | // Remove a Permissions Boundary that is inherited, for example from the Stack level
|
328 | declare const customResource: CustomResource;
|
329 | iam.PermissionsBoundary.of(customResource).clear();
|
330 | ```
|
331 |
|
332 | ## OpenID Connect Providers
|
333 |
|
334 | OIDC identity providers are entities in IAM that describe an external identity
|
335 | provider (IdP) service that supports the [OpenID Connect] (OIDC) standard, such
|
336 | as Google or Salesforce. You use an IAM OIDC identity provider when you want to
|
337 | establish trust between an OIDC-compatible IdP and your AWS account. This is
|
338 | useful when creating a mobile app or web application that requires access to AWS
|
339 | resources, but you don't want to create custom sign-in code or manage your own
|
340 | user identities. For more information about this scenario, see [About Web
|
341 | Identity Federation] and the relevant documentation in the [Amazon Cognito
|
342 | Identity Pools Developer Guide].
|
343 |
|
344 | [OpenID Connect]: http://openid.net/connect
|
345 | [About Web Identity Federation]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_oidc.html
|
346 | [Amazon Cognito Identity Pools Developer Guide]: https://docs.aws.amazon.com/cognito/latest/developerguide/open-id.html
|
347 |
|
348 | The following examples defines an OpenID Connect provider. Two client IDs
|
349 | (audiences) are will be able to send authentication requests to
|
350 | <https://openid/connect>.
|
351 |
|
352 | ```ts
|
353 | const provider = new iam.OpenIdConnectProvider(this, 'MyProvider', {
|
354 | url: 'https://openid/connect',
|
355 | clientIds: [ 'myclient1', 'myclient2' ],
|
356 | });
|
357 | ```
|
358 |
|
359 | You can specify an optional list of `thumbprints`. If not specified, the
|
360 | thumbprint of the root certificate authority (CA) will automatically be obtained
|
361 | from the host as described
|
362 | [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc_verify-thumbprint.html).
|
363 |
|
364 | Once you define an OpenID connect provider, you can use it with AWS services
|
365 | that expect an IAM OIDC provider. For example, when you define an [Amazon
|
366 | Cognito identity
|
367 | pool](https://docs.aws.amazon.com/cognito/latest/developerguide/open-id.html)
|
368 | you can reference the provider's ARN as follows:
|
369 |
|
370 | ```ts
|
371 | import * as cognito from '@aws-cdk/aws-cognito';
|
372 |
|
373 | declare const myProvider: iam.OpenIdConnectProvider;
|
374 | new cognito.CfnIdentityPool(this, 'IdentityPool', {
|
375 | openIdConnectProviderArns: [myProvider.openIdConnectProviderArn],
|
376 | // And the other properties for your identity pool
|
377 | allowUnauthenticatedIdentities: false,
|
378 | });
|
379 | ```
|
380 |
|
381 | The `OpenIdConnectPrincipal` class can be used as a principal used with a `OpenIdConnectProvider`, for example:
|
382 |
|
383 | ```ts
|
384 | const provider = new iam.OpenIdConnectProvider(this, 'MyProvider', {
|
385 | url: 'https://openid/connect',
|
386 | clientIds: [ 'myclient1', 'myclient2' ],
|
387 | });
|
388 | const principal = new iam.OpenIdConnectPrincipal(provider);
|
389 | ```
|
390 |
|
391 | ## SAML provider
|
392 |
|
393 | An IAM SAML 2.0 identity provider is an entity in IAM that describes an external
|
394 | identity provider (IdP) service that supports the SAML 2.0 (Security Assertion
|
395 | Markup Language 2.0) standard. You use an IAM identity provider when you want
|
396 | to establish trust between a SAML-compatible IdP such as Shibboleth or Active
|
397 | Directory Federation Services and AWS, so that users in your organization can
|
398 | access AWS resources. IAM SAML identity providers are used as principals in an
|
399 | IAM trust policy.
|
400 |
|
401 | ```ts
|
402 | new iam.SamlProvider(this, 'Provider', {
|
403 | metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
|
404 | });
|
405 | ```
|
406 |
|
407 | The `SamlPrincipal` class can be used as a principal with a `SamlProvider`:
|
408 |
|
409 | ```ts
|
410 | const provider = new iam.SamlProvider(this, 'Provider', {
|
411 | metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
|
412 | });
|
413 | const principal = new iam.SamlPrincipal(provider, {
|
414 | StringEquals: {
|
415 | 'SAML:iss': 'issuer',
|
416 | },
|
417 | });
|
418 | ```
|
419 |
|
420 | When creating a role for programmatic and AWS Management Console access, use the `SamlConsolePrincipal`
|
421 | class:
|
422 |
|
423 | ```ts
|
424 | const provider = new iam.SamlProvider(this, 'Provider', {
|
425 | metadataDocument: iam.SamlMetadataDocument.fromFile('/path/to/saml-metadata-document.xml'),
|
426 | });
|
427 | new iam.Role(this, 'Role', {
|
428 | assumedBy: new iam.SamlConsolePrincipal(provider),
|
429 | });
|
430 | ```
|
431 |
|
432 | ## Users
|
433 |
|
434 | IAM manages users for your AWS account. To create a new user:
|
435 |
|
436 | ```ts
|
437 | const user = new iam.User(this, 'MyUser');
|
438 | ```
|
439 |
|
440 | To import an existing user by name [with path](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-friendly-names):
|
441 |
|
442 | ```ts
|
443 | const user = iam.User.fromUserName(this, 'MyImportedUserByName', 'johnsmith');
|
444 | ```
|
445 |
|
446 | To import an existing user by ARN:
|
447 |
|
448 | ```ts
|
449 | const user = iam.User.fromUserArn(this, 'MyImportedUserByArn', 'arn:aws:iam::123456789012:user/johnsmith');
|
450 | ```
|
451 |
|
452 | To import an existing user by attributes:
|
453 |
|
454 | ```ts
|
455 | const user = iam.User.fromUserAttributes(this, 'MyImportedUserByAttributes', {
|
456 | userArn: 'arn:aws:iam::123456789012:user/johnsmith',
|
457 | });
|
458 | ```
|
459 |
|
460 | ### Access Keys
|
461 |
|
462 | The ability for a user to make API calls via the CLI or an SDK is enabled by the user having an
|
463 | access key pair. To create an access key:
|
464 |
|
465 | ```ts
|
466 | const user = new iam.User(this, 'MyUser');
|
467 | const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user });
|
468 | ```
|
469 |
|
470 | You can force CloudFormation to rotate the access key by providing a monotonically increasing `serial`
|
471 | property. Simply provide a higher serial value than any number used previously:
|
472 |
|
473 | ```ts
|
474 | const user = new iam.User(this, 'MyUser');
|
475 | const accessKey = new iam.AccessKey(this, 'MyAccessKey', { user: user, serial: 1 });
|
476 | ```
|
477 |
|
478 | An access key may only be associated with a single user and cannot be "moved" between users. Changing
|
479 | the user associated with an access key replaces the access key (and its ID and secret value).
|
480 |
|
481 | ## Groups
|
482 |
|
483 | An IAM user group is a collection of IAM users. User groups let you specify permissions for multiple users.
|
484 |
|
485 | ```ts
|
486 | const group = new iam.Group(this, 'MyGroup');
|
487 | ```
|
488 |
|
489 | To import an existing group by ARN:
|
490 |
|
491 | ```ts
|
492 | const group = iam.Group.fromGroupArn(this, 'MyImportedGroupByArn', 'arn:aws:iam::account-id:group/group-name');
|
493 | ```
|
494 |
|
495 | To import an existing group by name [with path](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html#identifiers-friendly-names):
|
496 |
|
497 | ```ts
|
498 | const group = iam.Group.fromGroupName(this, 'MyImportedGroupByName', 'group-name');
|
499 | ```
|
500 |
|
501 | To add a user to a group (both for a new and imported user/group):
|
502 |
|
503 | ```ts
|
504 | const user = new iam.User(this, 'MyUser'); // or User.fromUserName(stack, 'User', 'johnsmith');
|
505 | const group = new iam.Group(this, 'MyGroup'); // or Group.fromGroupArn(stack, 'Group', 'arn:aws:iam::account-id:group/group-name');
|
506 |
|
507 | user.addToGroup(group);
|
508 | // or
|
509 | group.addUser(user);
|
510 | ```
|
511 |
|
512 | ## Features
|
513 |
|
514 | * Policy name uniqueness is enforced. If two policies by the same name are attached to the same
|
515 | principal, the attachment will fail.
|
516 | * Policy names are not required - the CDK logical ID will be used and ensured to be unique.
|
517 | * Policies are validated during synthesis to ensure that they have actions, and that policies
|
518 | attached to IAM principals specify relevant resources, while policies attached to resources
|
519 | specify which IAM principals they apply to.
|