cedar-local-agent 3.0.0

Foundational library for creating Cedar-based asynchronous authorizers.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
# Cedar Local Agent

This crate is experimental.

The `cedar-local-agent` crate provides customers a useful foundation for creating asynchronous authorizers that
can handle two different operational modes:

1. The authorizer is **able** to cache all of your application’s policies and entity data while evaluating a request
2. The authorizer is **unable** to cache all of your application's policies and entity data while evaluating a request

The `cedar-local-agent` crate provides a [`simple::Authorizer`](./src/public/simple.rs) which can be built with option (1) or (2). The
[`simple::Authorizer`](./src/public/simple.rs) is constructed using policy and entity providers. These providers can be
implemented by customers.

`cedar-local-agent` provides sample implementations of providers that implement option (1). A file system policy set provider:
[`file::PolicySetProvider`](./src/public/file/policy_set_provider.rs), and an entity provider:
[`file::EntityProvider`](./src/public/file/entity_provider.rs).

For more information about the Cedar language/project, please take a look
at [cedarpolicy.com](https://www.cedarpolicy.com).

## Usage

Cedar Local Agent can be used in your application via the `cedar-local-agent` crate.

Add `cedar-local-agent` as a dependency in your `Cargo.toml` file. For example:

```toml
[dependencies]
cedar-local-agent = "2"
```

## Quick Start

Build a local authorizer that evaluates authorization decisions using a locally stored
policy set, entity store and schema.

Policy data: [`tests/data/sweets.cedar`](./tests/data/sweets.cedar)

Entity data: [`tests/data/sweets.entities.json`](./tests/data/sweets.entities.json)

Schema: [`tests/data/sweets.schema.cedar.json`](./tests/data/sweets.schema.cedar.json)

Build a policy set:

```rust
let policy_set_provider = PolicySetProvider::new(
    policy_set_provider::ConfigBuilder::default()
        .policy_set_path("tests/data/sweets.cedar")
        .build()
        .unwrap(),
)
.unwrap();
```

Build an entity provider:

```rust
let entity_provider = EntityProvider::new(
    entity_provider::ConfigBuilder::default()
        .entities_path("tests/data/sweets.entities.json")
        .schema_path("tests/data/sweets.schema.cedar.json")
        .build()
        .unwrap(),
)
.unwrap();
```

Build the authorizer:

```rust
let authorizer: Authorizer<PolicySetProvider, EntityProvider> = Authorizer::new(
    AuthorizerConfigBuilder::default()
        .entity_provider(Arc::new(entity_provider))
        .policy_set_provider(Arc::new(policy_set_provider))
        .build()
        .unwrap(),
);
```

Evaluate a decision:

```rust
assert_eq!(
    authorizer
        .is_authorized(&Request::new(
            Some(format!("User::\"Cedar\"").parse().unwrap()),
            Some(format!("Action::\"read\"").parse().unwrap()),
            Some(format!("Box::\"3\"").parse().unwrap()),
            Context::empty(),
        ), &Entities::empty())
        .await
        .unwrap()
        .decision(),
    Decision::Deny
)
```

## [`simple::Authorizer`]./src/public/simple.rs `is_authorized` API Semantics

The [`simple::Authorizer`](./src/public/simple.rs) `is_authorized` API takes a
[`Cedar request`](https://github.com/cedar-policy/cedar/tree/main/cedar-policy)
and [`Cedar entities`](https://github.com/cedar-policy/cedar/tree/main/cedar-policy) within the API.

```rust
pub async fn is_authorized(
    &self,
    request: &Request,
    entities: &Entities,
) -> Result<Response, AuthorizerError> { ... }
```

For scenarios where the same entity identifier, `EID`, is passed as input and returned by an `EntityProvider`, the input is
considered the last value. This API favors last-value-wins semantics.
This behavior is subject to change pending [`RFC-0020`](https://github.com/cedar-policy/rfcs/blob/main/text/0020-unique-record-keys.md).

## Updating [`file::PolicySetProvider`]./src/public/file/policy_set_provider.rs or [`file::EntityProvider`]./src/public/file/entity_provider.rs data

The [`file::PolicySetProvider`](./src/public/file/policy_set_provider.rs) and [`file::EntityProvider`](./src/public/file/entity_provider.rs)
gather data when initialized and cache it in memory. No data is read from disk during an authorization decision.

Policy and entity data can be mutated on disk after the initialization of an authorizer. To account for this, functionality
is provided which will refresh policy and entity data tangential to an authorization decision.
To accomplish this, a minimum of two additional threads are required, for a total of three threads.

1. The main thread handles `is_authorized` calls
2. The signaler thread notifies receivers of required updates
3. The receiver thread listens for updates

[`Channels`](https://doc.rust-lang.org/rust-by-example/std_misc/channels.html) are used to communicate between the signaler thread and
the receiver thread. There are two provided functions for creating signaler threads. Both return a signaler thread and a
[`tokio::broadcast::receiver`](https://docs.rs/tokio/latest/tokio/sync/broadcast/struct.Receiver.html) as an output.

1. [`clock_ticker_task`]./src/public/events/core.rs periodically wakes up and signals based on a clock duration
2. [`file_inspector_task`]./src/public/events/core.rs periodically wakes up and checks for differences in a file
   using a collision resistant hashing function (SHA256) and notifies on modifications

Warning: It is important to be careful when selecting the refresh rate of the signaler which triggers a policy refresh.
We have set up a `RefreshRate` enum which gives several options of RefreshRates of at least 15 seconds.
This is fast enough for most applications but slow enough to be very unlikely to trigger any sort of throttling or performance impact on most policy set sources.


Receivers are required to be passed to a new separate thread to listen and respond to events.
The [`update_provider_data_task`](./src/public/events/receive.rs) handles receiving these signals in the form of an
[`Event`](./src/public/events/mod.rs). Messages are handled one message at a time. The receiver thread blocks until
it has successfully or unsuccessfully updated the data for the provider.

Sample usage of updating a policy set provider's data every fifteen seconds:

```rust
let (clock_ticker_signal_thread, receiver) = clock_ticker_task(RefreshRate::FifteenSeconds);

let policy_set_provider = Arc::new(PolicySetProvider::new(
    policy_set_provider::ConfigBuilder::default()
        .policy_set_path("tests/data/sweets.cedar")
        .build()
        .unwrap(),
)
.unwrap());

let update_provider_thread = update_provider_data_task(policy_set_provider.clone(), receiver);
```

Note: these background threads must remain in scope for the life of your application. If there is an issue updating
in a background thread it will produce an `error!()` message but will not cause the application to crash.

This means that if for any reason, the agent cannot update its policy set due to an error, the agent will continue running with the stale policy set and will log an error.
Since the agent will log `error!()`'s, it is possible to configure log-based alarms so that these failures can be caught quickly, but that is outside the scope of this README.

## Tracing

This crate emits trace data [`tracing`](https://docs.rs/tracing/latest/tracing/) and can be integrated
into standard tracing implementations.

## Authorization Logging

Authorization logs are designed to power detection and response capabilities. Sample capabilities can be found
under the [`Mitre D3fend matrix`](https://d3fend.mitre.org/),
for example [`User Behavioral Analysis`](https://d3fend.mitre.org/technique/d3f:UserBehaviorAnalysis/).

The authorizer's provided emit authorization events as [`tracing events`](https://docs.rs/tracing/latest/tracing/struct.Event.html).
Authorization events are included in tracing [`spans`](https://docs.rs/tracing/latest/tracing/span/index.html).
Authorization events are default formatted using [`Open Cyber Security Format`](https://github.com/ocsf).
Authorization events can optionally be filtered, formatted and routed directly to an authorization log. See example:

```rust
// Dependencies must be included in the `Cargo.toml` file of the application
// tracing, tracing-appender, tracing-subscriber

let authorization_roller = tracing_appender::rolling::minutely("logs", "authorization.log");
let (authorization_non_blocking, _guard) = tracing_appender::non_blocking(authorization_roller);
let authorization_log_layer = tracing_subscriber::fmt::layer()
    .event_format(AuthorizerFormatter(AuthorizerTarget::Simple))
    .with_writer(authorization_non_blocking);
tracing_subscriber::registry()
    .with(authorization_log_layer)
    .try_init()
    .expect("Logging Failed to Start, Exiting.");
```

To filter authorization event logs, provide a log config to the authorizer with a `FieldSet` which includes the fields that are to be logged. By default if not explicitly configured, no fields will be logged.

Sample usage of logging everything within the authorization request. **Note**: Logging everything is insecure; please see [Secure Logging Configuration](#secure-logging-configuration). 

```rust
let log_config =
    log::ConfigBuilder::default()
        .field_set(log::FieldSetBuilder::default()
            .principal(true)
            .action(true)
            .resource(true)
            .context(true)
            .entities(log::FieldLevel::All)
            .build()
            .unwrap())
    .build()
    .unwrap();

let authorizer: Authorizer<PolicySetProvider, EntityProvider> = Authorizer::new(
    AuthorizerConfigBuilder::default()
        .entity_provider(...)
        .policy_set_provider(...)
        .log_config(log_config)
        .build()
        .unwrap(),
);
```

Note: ``Authorizer`` ``log_config`` ``FieldSet::entities `` refers to the Cedar entities. 
There is also an [OCSF](https://github.com/ocsf) field called [``entity``](https://github.com/cedar-policy/cedar-local-agent/blob/main/src/public/log/schema.rs#L86) which refers to the principal entity that is sending the request.

This means that when ``Authorizer`` ``log_config`` ``FieldSet::entities`` is set to ``FieldLevel::None``, the OCSF entity will still be logged. 
This is not a bug and is expected behaviour.

For more examples of how to set up the authorization logging, see our [usage examples](https://github.com/cedar-policy/cedar-local-agent/tree/main/examples/tracing/authorization_log)

### Secure Logging Configuration:

Using a `log::FieldSet` configuration that sets any cedar-related field (principal, action, resource, context, and entities) to `true` will result in that field being logged. 
These fields could contain sensitive information and should be exposed with caution. Additionally, the cedar language has no current limit on field sizes within a [`Request`](https://docs.rs/cedar-policy/2.4.0/cedar_policy/struct.Request.html). 
A large request with verbose logging can result in more disk i/o to occur. This disk i/o could negatively impact the performance of the application.

For a safe config, create a default `log::FieldSet` with `log::FieldSetBuilder::default().build().unwrap()`. 
This option will redact user input like so: 

`"entity":{"data":{"Parents":[]},"name":"Sensitive<REDACTED>","type":"Sensitive<REDACTED>"}`

### Note:

Cedar does not at this time support extracting the `Context` from the `Request` struct since it is private, therefore it is extracted using `request.to_string()`. This is not ideal as this logs the entire request (Principal, Action, Resource, Context) instead of just the context.

In particular this brings two quirks:

- If the `FieldSet` has principal and context set to true, then the resulting log will include the principal twice.
- If the `FieldSet` has principal set to false and context set to true, then the resulting log will include principal anyway since it is included in the `request.to_string()` call required to extract the context.

The above are not specific to principal and also occur with action and resource. A cedar github issue has been created to add a getter for the context on the cedar Request struct that will fix this: https://github.com/cedar-policy/cedar/issues/363

## Example application

This project is based on a fictitious application that allows users to manage sweet boxes. There are two entities:

1. Box (Resource)
2. User (Principal)

There are two user entities:

1. Eric
2. Mike

There are ten resources, `Box`. Each box has an entity identifier (id) that ranges
between the numbers 1-10. Each `Box` has one attribute `owner`. The owners of each `Box` are defined in
the [`tests/data/sweets.entities.json`](./tests/data/sweets.entities.json) file,
this file represents the complete database of information for this application.

The actions that `Users` can perform on each `Box` are defined in the schema file:
[`tests/data/sweets.schema.cedar.json`](./tests/data/sweets.schema.cedar.json). To summarize, each `User`
can perform one of the following actions `read`, `update` or `delete` on a `Box` resource.

A policy is a statement that declares which `Users` are explicitly permitted, or explicitly forbidden to perform an
action on a resource, `Box`. Here is a sample policy:

```
@id("owner-policy")
permit(principal, action, resource)
when { principal == resource.owner };
```

Refer to [`tests/data/sweets.cedar`](./tests/data/sweets.cedar) for the full details of these policies.
This file represents all policies for this application.

Given the `schema`, `entities` and `policy_set` the application can use the `Authorizer` as provided in the usage above.
Here is a sample request and expected outcome:

```rust
 assert_eq!(
     authorizer
         .is_authorized(&Request::new(
             Some(format!("User::\"Mike\"").parse().unwrap()),
             Some(format!("Action::\"read\"").parse().unwrap()),
             Some(format!("Box::\"3\"").parse().unwrap()),
             Context::empty(),
         ), &Entities::empty())
         .await
         .unwrap()
         .decision(),
     Decision::Deny
 );
 assert_eq!(
     authorizer
         .is_authorized(&Request::new(
             Some(format!("User::\"Mike\"").parse().unwrap()),
             Some(format!("Action::\"read\"").parse().unwrap()),
             Some(format!("Box::\"2\"").parse().unwrap()),
             Context::empty(),
         ), &Entities::empty())
         .await
         .unwrap()
         .decision(),
     Decision::Allow
 );
```

Feel free to refer to this sample application within the integration test located here: [`tests/lib.rs`](./tests/lib.rs).

## General Security Notes

The following is a high level description of some security concerns to keep in mind when using the `cedar-local-agent`
to enable local evaluation of Cedar policies stored on a local file system.

### Trusted Computing Environment

The `cedar-local-agent` is a mere library that customers can wrap in say an HTTP server and deploy onto a fleet of hosts.
It is, therefore, left to users to take any and all necessary precautions to ensure those security concerns beyond what
the `cedar-local-agent` is capable of enforcing are met. This includes:

1. Filesystem permissions for on-disk Policy Stores should be limited to least-privilege, see [Limiting Access to Local Data Files]#limiting-access-to-local-data-files.
2. Filesystem permissions for on-disk locations of OCSF logs follow least-privilege permissions, see [OCSF Log directory permissions]#ocsf-log-directory-permissions.
3. The `cedar-local-agent` is configured securely, see [Quick Start]#quick-start and [Updating `file::PolicySetProvider` or `file::EntityProvider` data]#updating-filepolicysetprovider-or-fileentityprovider-data for configuration best practices.
4. Validating the size of files read and requests made to `is_authorized` to prevent potential denial of service.

### Limiting Access to Local Data Files

The local authorizer provided in this crate only needs **read** access to locally stored policy set, entity store and
schema files.

Write access to local data files (policies, entities and schema) should be restricted only to users that really
need to make changes to these files, for example, to add new entities and remove old policies.

In the case where there are no restrictions to access local data files, a malicious Operating System (OS) user can add or
remove policies, modify entities attributes, make slight changes that are hard to identify, or even change the policies
to deny all actions. To illustrate this possibility, consider a cedar file with the following cedar policies from the
[Example Application](## Example application):

```
@id("mike-edit-box-1")
permit (
    principal == User::"Mike",
    action == Action::"update",
    resource == Box::"1"
);

@id("eric-view-box-9")
permit (
    principal == User::"Eric",
    action == Action::"read",
    resource == Box::"9"
);
```

In this example, principal "Mike" is allowed to perform "update" on resource box "1" while principal "Eric" is allowed to
perform "read" on resource box "9". Now, consider a malicious OS user adding the statement below to the same policies file.

```
@id("ill-intentioned-policy")
forbid(principal, action, resource);
```

In the next policies file refresh cycle, the [`file::PolicySetProvider`](./src/public/file/policy_set_provider.rs) will refresh policies file content to memory,
and the local authorizer will deny any action from any principal.

#### How to avoid this problem from happening?

In order to prevent this kind of security issue, you must restrict read access to the data files, and more important,
restrict write access to these files. Only users or groups that really need to write changes to policies,
or entities should be allowed to do so (for example, another agent that fetches policies from an internal application).

For one example on how to avoid this problem, say you have the following folder structure for a local-agent built with
`cedar-local-agent` crate.

```
authz-agent/
  |- authz_daemon (executable)

authz-local-data/
  |- policies.cedar
  |- entities.json
  |- schema.json
```

Now suppose you have an OS user to execute the "authz_daemon" called "authz-daemon" from user group "authz-ops".
And you have a user called "authz-ops-admin" from the same user group "authz-ops" that will be able to update data files.

Then, make "authz-ops-admin" the owner of **authz-local-data** folder with:

```bash
$ chown -R authz-ops-admin:authz-ops authz-local-data
```

And make "authz-daemon" user the owner of **authz-agent** folder with:

```bash
$ chown -R authz-daemon:authz-ops authz-agent
```

Finally, make **authz-local-data** readable by everyone and writable by the owner only:

```bash
$ chmod u=rwx,go=r authz-local-data
```

### OCSF Log directory permissions


The local authorizer provided in this crate will require **read** and **write** access to the directory where it will write OCFS logs to.

Suppose we have the following directory structure:

```
authz-agent/
  |- authz_daemon (executable)

ocsf-log-dir/
  |- authorization.log.2023-11-15-21-02
  ...
```

Now suppose you have an OS user to execute the **authz_daemon** called **authz-daemon** which should be in a group called "log-reader".

And make **authz-daemon** user the owner of  **ocsf-log-dir** folder with:

```bash
$ chown -R authz-daemon:log-reader ocsf-log-dir
```

We will now make **ocsf-log-dir** readable and writable by the owner but not writable to anyone else.
We allow anyone in the **log-reader** group to read the contents of the folder but not write to it.

```bash
$ chmod u=wrx,g=rx,o= ocsf-log-dir
```

NOTE: We need to allow **execute** permissions in order to access files in the directory.

Any agent that needs to access the logs, such as the [AWS Cloudwatch Agent](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Install-CloudWatch-Agent.html) should run as a user in the log-reader group so that they will have the proper access (see [documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch-Agent-common-scenarios.html) for how to configure the Cloudwatch Agent to run as a certain user).

## Getting Help

- [GitHub issues]https://github.com/cedar-policy/cedar-local-agent/issues
- [Cedar documentation]https://docs.cedarpolicy.com/
- [Usage examples]examples

## License

This project is licensed under the Apache-2.0 License.