-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Craig Disselkoen <[email protected]>
- Loading branch information
1 parent
819b9aa
commit 2929508
Showing
1 changed file
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,287 @@ | ||
# Cedar Standard Library | ||
|
||
## Related issues and PRs | ||
|
||
- Reference Issues: | ||
- Implementation PR(s): | ||
|
||
## Timeline | ||
|
||
- Started: 2024-03-12 | ||
|
||
## Summary | ||
|
||
Create a Cedar Standard Library of type declarations that are likely to be | ||
useful to many Cedar users, and are distributed along with Cedar. | ||
|
||
Allow users to "extend" the type declarations in the standard library by adding | ||
more parent types and/or more attributes. | ||
|
||
This RFC includes the concept/infrastructure for the Cedar Standard Library, and | ||
also concretely proposes one type declaration to be included in it: `OIDC::User`. | ||
Discussion of other type declarations to add to the Cedar Standard Library is | ||
left out of scope for this RFC. | ||
|
||
## Basic example | ||
|
||
``` | ||
type User = __cedar::OIDC::User; | ||
``` | ||
or | ||
``` | ||
entity Group { myAttribute: String }; | ||
entity User extends __cedar::OIDC::User in [Group] { emailOptOut: Boolean }; | ||
``` | ||
|
||
## Motivation | ||
|
||
Some data sources are common across many applications and useful to many Cedar | ||
users. | ||
The running (motivating) example for this RFC is identity providers (IdPs) which | ||
comply with the OpenID Connect standard (OIDC). | ||
The OIDC standard includes a list of attributes that exist on a user type; this | ||
is naturally declared as a Cedar entity type. | ||
|
||
By including declarations for types like this in a standard library, we | ||
accomplish three goals: | ||
1. Saving each Cedar user the effort of writing the declaration themselves. | ||
This facilitates code reuse in schemas, and makes it easier to get started | ||
with Cedar. | ||
2. Ensuring the community's alignment on the "best" way to declare each type. | ||
For instance, should OIDC `updatedAt` be expressed as a `String`, as a `Long` | ||
(eg Unix timestamp), or as a record with multiple components? | ||
This allows Cedar to encourage best practices. | ||
3. Facilitating code reuse for Cedar authorization calls, not just schemas. | ||
When everyone shares a common definition of `OIDC::User`, the community could | ||
conceivably converge on a reusable library function for, e.g., converting an | ||
OIDC token into Cedar entity data. | ||
This would further make it easier to get started with Cedar. | ||
(Note that this RFC does not propose Cedar writing or maintaining library | ||
functions like this, it only points out that the community could converge on | ||
library functions like this. | ||
This RFC only proposes a standard library for schema declarations.) | ||
|
||
## Detailed design | ||
|
||
The Cedar standard library declarations will all live in the namespace `__cedar` | ||
(previously reserved as part of [RFC 24]; see also [RFC 52]). | ||
|
||
Cedar standard library declarations will be automatically available for use in | ||
every Cedar schema. Under the hood, this means they are automatically prepended | ||
to every Cedar schema, much like a standard library or "prelude" in other | ||
languages. For ease of maintainability, they should probably exist in the Cedar | ||
repo as literal Cedar schema files, which are loaded and parsed either at Cedar | ||
compile-time, or at application startup with `lazy_static`. | ||
|
||
This RFC also includes the new keyword `extends`, allowing a schema to extend a | ||
previously existing entity declaration by adding more parent types and/or more | ||
attributes. | ||
The syntax for this is (informally) as follows: | ||
``` | ||
entity MyEntityType extends BaseEntityType; // allowed, functionally equivalent to `type MyEntityType = BaseEntityType;` | ||
entity MyEntityType extends BaseEntityType in [MyGroupType]; // `MyEntityType` has all the attributes and parent types of `BaseEntityType`, and also can be a member of `MyGroupType` | ||
entity MyEntityType extends BaseEntityType { emailOptOut: Boolean }; // `MyEntityType` has all the attributes and parent types of `BaseEntityType`, and also has an attribute `emailOptOut` of type `Boolean` | ||
entity MyEntityType extends BaseEntityType in [MyGroupType] { emailOptOut: Boolean }; // combining the above examples | ||
``` | ||
|
||
The base type may be an entity type, or may be a common type that aliases an | ||
entity type. | ||
As a consequence of this rule, chains of `extends` are allowed: you can write | ||
`entity B extends A` and later `entity C extends B`. | ||
|
||
To avoid cycles, the base type for `extends` must be a previously declared type | ||
(as in, declared farther up in the schema file). | ||
For this purpose, standard library declarations are considered to appear before | ||
line 1 of the schema file. | ||
If the base type is defined in a different schema file, that other schema file | ||
must be loaded first (e.g., must appear earlier in the iterator in [`Schema::from_schema_fragments()`]). | ||
|
||
## Drawbacks | ||
|
||
1. Cedar commits to maintaining all of the definitions in the Cedar standard | ||
library, which is a (hopefully small) maintenance burden. We can weigh this | ||
burden against the utility for each individual type declaration proposed for the | ||
Cedar standard library. This RFC only proposes the idea of the standard library, | ||
and a single definition `OIDC::User`. | ||
2. If a single declaration for `OIDC::User` (or other type) can't be agreed upon | ||
by the community, it may be difficult for Cedar to decide which definition | ||
should be in the standard library. Cedar may have to come up with best practices | ||
or tenets for resolving such disputes. We also could theoretically end up with a | ||
situation where part of the community uses the standard definition and another | ||
part uses a competing definition, which may or may not be better than how things | ||
would develop organically without this RFC. | ||
3. Users may incorrectly assume that `extends` has some kind of impact at | ||
runtime -- for instance that concrete values of `MyEntityType` inherit attribute | ||
values from a concrete value of `BaseEntityType` somehow. | ||
Hopefully we can address this with clear documentation. | ||
There's also nothing preventing us from providing API helpers that allow | ||
extending concrete `Entity` objects of a base type corresponding with a schema | ||
`extends` declaration (although this is not proposed in this RFC). | ||
|
||
This is not a breaking change for any existing Cedar users. | ||
All existing valid Cedar schemas remain valid. | ||
|
||
## Alternatives | ||
|
||
### Alternative A: Distribute standard library as ordinary schema files | ||
|
||
Instead of having the declarations of types like `OIDC::User` implicitly | ||
available as part of Cedar, Cedar could just distribute ordinary schema files | ||
with these declarations (in the `cedar-policy/cedar` repo or elsewhere). | ||
Users would have to provide this schema file in addition to the rest of their | ||
schema. (Note that Cedar already supports schemas spread over multiple files.) | ||
|
||
Users could still use this RFC's `extends` mechanism to extend the standard | ||
declarations, but they would probably be tempted to just modify the distributed | ||
schema files directly instead. | ||
|
||
### Alternative B: Only allow extending the standard-library entity declarations | ||
|
||
As written, this RFC allows `extends` with any entity declarations, for | ||
consistency and least-surprise. | ||
We could instead only allow `extends` to be used with specifically the | ||
standard-library types. | ||
This would still leave the door open to going back to the current proposal in | ||
the future, without a breaking change. | ||
|
||
However, I don't see any good reason to restrict `extends` at this time. | ||
Allowing `extends` to work with all types might facilitate third-party | ||
ecosystems of standard declarations (distributed as schema files, a la | ||
Alternative A) which folks could use instead of or in addition to the Cedar | ||
standard library. | ||
Some folks might also find it locally useful within their own schemas. | ||
|
||
### Alternative C: Explicit declaration to use/import the standard library | ||
|
||
This RFC doesn't currently propose require any kind of `use` or `import` | ||
statement in order to have access to the Cedar standard library. | ||
Rather, the Cedar standard library is always implicitly available in the | ||
`__cedar` namespace. | ||
An alternate design would require an explicit `use` or `import` statement | ||
in order to make these declarations available / active. | ||
One reason to do this would be in anticipation of a possible future where | ||
we allow `use` or `import` with libraries other than the standard library | ||
(and other than libraries provided as ordinary schema files, a la Alternative | ||
A), or if we are afraid of the performance cost of implicitly including the | ||
standard library in every schema. | ||
|
||
However, I don't see a good reason to require an explicit `use` or `import` | ||
statement for the standard library. | ||
Even if a future RFC introduces a `use` or `import` statement, as envisioned | ||
above, I think it would still be reasonable to have the standard library | ||
available automatically, without such a statement. | ||
|
||
### Alternative D: Explicit mechanism to use/open a namespace | ||
|
||
This RFC doesn't currently propose any (new) mechanism to use/open a namespace. | ||
One might think it would be desirable to have something like | ||
`use __cedar::OIDC::User;` or `open __cedar::OIDC::User;` to allow referring to | ||
the standard library type `__cedar::OIDC::User` as just `User` in the schema. | ||
But we already do have such a mechanism, common types: users can write | ||
``` | ||
type User = __cedar::OIDC::User; | ||
``` | ||
and then refer to the OIDC User type as just `User` in their schema. | ||
This is arguably more explicit and clearer, and also avoids introducing a | ||
redundant mechanism for something that is already possible using existing | ||
syntax. | ||
|
||
This is an optional added feature; nothing in this RFC prevents us from | ||
introducing a mechanism like this in the future. | ||
|
||
### Alternative E: Implicitly search the `__cedar` namespace | ||
|
||
Currently, when a user writes a type `A`, the schema parser searches the current | ||
active namespace for a declaration `A`, then if not found, it searches the empty | ||
namespace for a declaration `A`. | ||
We could add a third step to this chain, that if `A` is not found in the empty | ||
namespace either, we could search the `__cedar` namespace for a declaration `A`. | ||
The end result of this would be that you could write `OIDC::User` instead of | ||
`__cedar::OIDC::User`, in any schema that didn't itself define an `OIDC` | ||
namespace. | ||
The `__cedar` prefix would only be necessary for disambiguation, which matches | ||
how it is used in [RFC 24]. | ||
|
||
This is an optional added feature; nothing in this RFC prevents us from | ||
introducing this search behavior in the future. | ||
|
||
TODO: does today's resolution algorithm do the above for namespaces `A`, or only | ||
for types `A`? | ||
My reading of [RFC 24] is that it only does this for unqualified types `A`; | ||
when the user writes `A::B::C`, and suppose the active namespace is `NS`, that | ||
only refers to the type `A::B::C`, and not `NS::A::B::C` (even if `NS::A::B::C` | ||
exists). | ||
If my reading is correct, we also would want to extend this search behavior to | ||
namespaces, so that in the above case, in addition to looking for `A::B::C` and | ||
`NS::A::B::C` (probably in that order, for backwards compatibility), we would | ||
then look for `__cedar::A::B::C` if neither of the first two were found. | ||
|
||
### Alternative F: Envisioning multiple inheritance | ||
|
||
This RFC doesn't currently propose to allow multiple inheritance (one type | ||
extending multiple different types at once). | ||
Multiple inheritance would be straightforward in simple cases (just take the union | ||
of all attributes and parent types), but tricky in some edge cases (what happens | ||
if two base types declare the same attribute but with different types?). | ||
|
||
Multiple inheritance is an optional added feature; nothing in this RFC prevents | ||
us from introducing it in the future. | ||
However, in anticipation of a possible multiple-inheritance feature, we could | ||
require brackets around the `extends` list, like so: | ||
``` | ||
entity MyEntityType extends [BaseEntityType] in ... | ||
``` | ||
This would allow us to easily support multiple base entity types in the future, | ||
syntactically. | ||
|
||
However, even if we take this RFC as-is today, it would be perfectly reasonable | ||
to introduce multiple inheritance in the future and keep this RFC's syntax valid | ||
for single inheritance. | ||
The brackets would be optional for single inheritance and required for multiple | ||
inheritance. | ||
|
||
### Alternative G: Other ways to handle inheritance cycles | ||
|
||
This RFC proposes that we avoid inheritance cycles by requiring that the base | ||
type for `extends` must be a previously declared type (as in, declared farther | ||
up in the schema file). | ||
This would be the first schema feature for which the order of declarations in | ||
the schema file matters (as well as the order of schema files in | ||
[`Schema::from_schema_fragments()`]). | ||
An alternate design would be to simply disallow cycles, without introducing an | ||
order restriction. | ||
That is, if there is an inheritance cycle, the schema is illegal, but if there | ||
is not, the schema is allowed. | ||
|
||
This is strictly more permissive than the RFC's main proposal, so nothing | ||
prevents us from moving from the RFC's main proposal to this alternative in the | ||
future. | ||
|
||
I'm unclear which of these rules is easier to implement in practice, or which | ||
results in clearer errors for users. | ||
|
||
### Alternative H: Bikeshedding the `extends` syntax | ||
|
||
This RFC proposes that `extends` must appear before `in` and attributes: | ||
``` | ||
entity MyEntityType extends A in [B]; | ||
``` | ||
We could instead require `extends` to appear _after_ `in` but before attributes: | ||
``` | ||
entity MyEntityType in [B] extends A; | ||
``` | ||
Or we could allow both. | ||
|
||
We could also consider some keyword other than `extends`, or some syntax other | ||
than a keyword, for example: | ||
``` | ||
entity MyEntityType:A in [B]; | ||
``` | ||
I'm personally partial to an `extends` keyword. | ||
|
||
[RFC 24]: https://github.com/cedar-policy/rfcs/blob/main/text/0024-schema-syntax.md | ||
[RFC 52]: https://github.com/cedar-policy/rfcs/pull/52 | ||
[`Schema::from_schema_fragments()`]: https://docs.rs/cedar-policy/latest/cedar_policy/struct.Schema.html#method.from_schema_fragments |