Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
251 changes: 251 additions & 0 deletions text/0101-nested-namespaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
# Allow Nested Namespaces in Cedar Schemas

## Related issues and PRs

- Reference Issues:
- Implementation PR(s):

## Timeline

- Started: 2025-08-22
- Accepted: TBD
- Stabilized: TBD

## Summary

As of today, Cedar does not allow for defining nested namespaces. Instead, a schema writer must define fully qualified namespaces at the top-level of a schema file. At times this can be unintuitive and make it difficult to write schemas with proper name hygiene.

## Basic example

Suppose, we want to write a Schema for defining entity's and types that are grouped in a hierarchical structure (e.g., representing components of an organization). Consider the below example schema that defines entity types for AWS's and Google's identity and storage services.

```
namespace aws {
namespace iam {
entity User {
department: String,
role: String,
clearanceLevel: Long
}
entity Group {
groupType: String,
maxClearance: Long
}
}

namespace s3 {
entity Bucket {
region: String,
sensitivity: Long
}

entity Object {
type: String,
owner: iam::User
}
}
}

namespace google {
namespace identity {
entity User {
email: String,
organizationId: String,
}
entity ServiceAccount {
displayName: String,
projectId: String,
}
}

namespace storage {
entity Bucket {
projectId: String,
location: String,
storageCLass: String,
}
entity Object {
contentType: String,
owner: identity::User,
}
}
}
```

Note, that in today, the above schema is not possible due to nested namespaces. Instead, one must write the above schema as:

```
namespace aws::iam {
entity User {
department: String,
role: String,
clearanceLevel: Long
}
entity Group {
groupType: String,
maxClearance: Long
}
}

namespace aws::s3 {
entity Bucket {
region: String,
sensitivity: Long
}

entity Object {
type: String,
owner: aws::iam::User
}
}

namespace google::identity {
entity User {
email: String,
organizationId: String,
}
entity ServiceAccount {
displayName: String,
projectId: String,
}
}

namespace google::storage {
entity Bucket {
projectId: String,
location: String,
storageCLass: String,
}
entity Object {
contentType: String,
owner: google::identity::User,
}
}
```

While the current namespaces make it possible to distinguish between the similarly named `aws::iam::User` and `google::identity::User` and `aws::s3::Bucket` and `google::storage::Bucket`. It is unintuitive that the sub-namespaces cannot be nested in a single namespaces for `aws` and `google`, respectively and similarly that one cannot refer to `iam::User` inside `aws::s3` without fully qualifying the name as `aws::iam::User`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tiny nit: this paragraph probably belongs in the Motivation section


## Motivation

In authorization, many types are naturally structured in a hierarchy (e.g., why we support defining hierarchies of identities). As such, it would be more intuitive for users to be allowed to define hierarchical namespaces that are explicitly nested that allow referencing those related resources via relative paths. Programmers naturally (and I think rightfully), expect to be able to define namespaces and types in a structured way.

## Detailed design

This RFC proposes updating the parser and name resolution algorithm for Cedar Schemas to enable nested namespaces with relative namespace resolution (for nested namespaces). Consider the example from above (with AWS and Google identity and storage entity types). This RFC proposes that both schemas are accepted and once parsed result in equal Schemas.
Comment on lines +132 to +134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this RFC propose any changes to name resolution in the JSON schema format? or any changes to what defines a valid schema in the JSON format?

Copy link
Author

@chaluli chaluli Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does not. But I would be open to changing this RFC to propose the JSON schemas be recursively definable structures that match the changes to human readable schema format. But, I think Json isn't really human readable and as such would not benefit as much from the purpose of this RFC.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this, but the RFC should say so explicitly. It should also state whether we preserve the property that all valid schemas in one format can be translated into a valid schema in the other format (I assume we do).


### Updated Schema Syntax

This RFC proposes that a Schema is a sequence of `Decl`s, a namespace is a namespace declaration containing a sequence of `Decls` and a `Decl` may now be a `Namespace`. This is opposed to the current grammar in which a Schema is a sequence of `Namespaces` which is either a namespace declaration containing a sequence of `Decl`s or a `Decl` without any surrounding namespace (i.e., within the root level namespace).

The proposed grammar will allow for both nested namespaces (new) and fully qualified namespaces at the top level (current).

```
Annotation := '@' IDENT '(' STR ')'
Annotations := {Annotations}
Schema := {Decl}
Decl := Entity | Action | TypeDecl | Namespace
Namespace := Annotations 'namespace' Path '{' {Decl} '}'
Entity := Annotations 'entity' Idents ['in' EntOrTyps] [['='] RecType] ['tags' Type] ';' | Annotations 'entity' Idents 'enum' '[' STR+ ']' ';'
Action := Annotations 'action' Names ['in' RefOrRefs] [AppliesTo]';'
TypeDecl := Annotations 'type' TYPENAME '=' Type ';'
Type := Path | SetType | RecType
EntType := Path
SetType := 'Set' '<' Type '>'
RecType := '{' [AttrDecls] '}'
AttrDecls := Annotations Name ['?'] ':' Type [',' | ',' AttrDecls]
AppliesTo := 'appliesTo' '{' AppDecls '}'
AppDecls := ('principal' | 'resource') ':' EntOrTyps [',' | ',' AppDecls]
| 'context' ':' RecType [',' | ',' AppDecls]
Path := IDENT {'::' IDENT}
Ref := Path '::' STR | Name
RefOrRefs := Ref | '[' [RefOrRefs] ']'
EntTypes := Path {',' Path}
EntOrTyps := EntType | '[' [EntTypes] ']'
Name := IDENT | STR
Names := Name {',' Name}
Idents := IDENT {',' IDENT}

IDENT := ['_''a'-'z''A'-'Z']['_''a'-'z''A'-'Z''0'-'9']*
TYPENAME := IDENT - RESERVED
STR := Fully-escaped Unicode surrounded by '"'s
PRIMTYPE := 'Long' | 'String' | 'Bool'
WHITESPC := Unicode whitespace
COMMENT := '//' ~NEWLINE* NEWLINE
RESERVED := 'Bool' | 'Boolean' | 'Entity' | 'Extension' | 'Long' | 'Record' | 'Set' | 'String'
```


### Why relative name resolution for only structurally nested namespaces and not any namespace with the same path prefix?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to consider the interaction with reopening namespace?

in one fragment

namespace Foo {
  entity A;
  namespace Bar {
    entity B;
  }
}

and then in another fragment

namespace Foo {
  namespace Bar {
    type C = A;
  }
}

and then what if we start allowing you to reopen a namespace inside the same fragment (don't think you can do that now, but could be wrong)


Consider the schema:

```
namespace Bar {
entity Thing; // (1)
}

namespace Foo::Bar {
entity Thing; // (2)
}

namespace Foo {
type MyThing = Bar::Thing;
}
```

Today, this is a currently valid schema. In which `Foo::MyThing` is a common type referring to `Bar::Thing` (definition 1). If we were to do relative name resolution based on namespaces with equal path prefixes, then `Foo::MyThing` would instead be a common type referring to `Foo::Bar::Thing` (definition 2), which would be backwards compatible.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at the end of this paragraph do you mean "backwards incompatible"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I got confused by this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we follow the algorithm below, I'd expect Bar::Thing still refers to (1) because the names of scope Foo only contain MyThing. Am I missing anything?


However, this RFC proposes that for nested namespaces, most users would expect definition 2 would be used. This RFC proposes that for structurally nested namespaces, definitions defined within the namespace declaration shadow namespaces outside of the namespace declaration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One concern: Will it be confusing that the schemas above and below this paragraph are not equivalent? Users might expect them to be equivalent?

Copy link
Author

@chaluli chaluli Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps. But I already think the choice of name resolution in cedar is confusing. In that, when I first looked into schemas, I expected the one above to behave as the one below.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the question is, are things more confusing after this RFC than the status quo. The argument on the side of this RFC would be that now things behave more intuitively in the case that you write nested namespaces. The argument against this RFC would be that having both of these schemas legal, but not equivalent, is more confusing than simply disallowing the latter and forcing everyone to write more namespaces explicitly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could be more explicit here by indicating what option (1 or 2) would MyThing resolve to.


```
namespace Bar {
entity Thing; // (1)
}

namespace Foo {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will we allow multiple instances of the same namespace declaration? I guess we would have to in order to be backwards compatible

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today, you cannot define the same namespace multiple times in the same schema fragment. See cedar-policy/cedar#1086

namespace Bar {
entity Thing; // (2)
}
type MyThing = Bar::Thing;
}
```

Similarly, for the below schema, this RFC proposes that `Foo::Bar::Baz::MyUser` be a common type that references definition 2, because it's nested namespace scope is the closest enclosing scope to the common type declaration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there anything special about this case? It seems that as a general rule you just use "closest scope" as resolution.


```
namespace Foo {
entity User; // (1)
namespace Bar {
entity User; // (2)

namespace Baz {
type MyUser = User;
}
}
```

### Name Resolution Algorithm

Below is some pythonic pseudo-code for determining name resolution (simplified to only concern the portions of name resolution that are due to nested namespace, e.g., not dealing with specifics of common types, cedar_types, etc.).

The algorithm is: first check the current namespace for the name. If it's not found in the current namespace check for any containing namespaces (note, a namespace only has a parent if it is a nested namespace), and otherwise search the global namespace for the name's definition.

```
resolve(name, this_scope, global_decls):
if name in this_scope:
return this_scope[name]
else if this_scope.has_parent():
return resolve(name, this_scope.parent(), global_decls):
else:
return global_decls[name]
```

## Drawbacks

While this change is likely to improve how one writes schemas, we should support nested schemas in a backwards compatible way. This means that, how we perform name resolution may be tricky in order to support backwards compatibility. This may either (1) limit how much users can exploit nested namespaces to use relative namespace identifies when defining entities and types or (2) increase the complexity of name resolution which may slow down parsing of Cedar Schemas.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on this? What specific corner cases in the current name resolution system could cause backwards compatibility issues when this feature is implemented?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A basic example of what problems can occur is that if we support relative name resolution for nested namespaces is found in the paragraph on lines 195-198, it would only work for namespaces that are structurally nested. Current "nested" namespaces (e.g., ones in which the path is a prefix of another) do not have the same resolution structure.


## Alternatives

The primary alternative is not accepting this rfc and instead keeping the status quo. I believe the current status quo is a worse user experience when writing a Cedar schema but would not encounter the drawbacks stated above.

An alternative to defaulting to use names (resolving to the name within the closest containing namespace), we could instead add explicit path identifiers for the current or containing scopes, e.g., `:this::User` and `:super::User` to help disambiguate relative versus global paths.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we elaborate more on the namespace-based resolution which would be backwards incompatible? Is there any reason this would be the right choice long term?