Skip to main content

Migrating Models To Schema 1.1

note
Fine Grained Authorization (FGA) is the early-stage product we are building at Okta to solve fine-grained authorization at scale. Sign up for the Developer Community Preview to try it out, and join our Discord community if you are interested in learning more about our plans.

Please note that at this point in time, it is not considered production-ready and does not come with any SLAs; availability and uptime are not guaranteed. Limitations of Okta FGA during the Developer Community Preview can be found here.
Okta FGA has introduced a new DSL schema version with several changes that we believe will make models easier to read and write, enable better tuple and model validations, and provide more options for optimizing the performance of different Okta FGA APIs.

In short, we’ll be:

  1. Adding type restrictions.
  2. Removing the need to specify as self, and
  3. Requiring you to specify for which relations you can write tuples with public access (using ‘*’).

Okta FGA Model Schema Versions

Since the changes in the DSL are significant we have decided to add a schema version to the DSL. The previous version of the DSL’s schema was 1.0, and the new schema version will be 1.1. To use the new syntax please add the following to the top of the model:

model
schema 1.1
Okta FGA will eventually stop supporting schema version 1.0. Notifications will be posted in GitHub, Discord and Twitter before this change occurs.

Type Enforcements & Removing as self

We’ll use the following version 1.0 model and tuples to illustrate the changes we’ll need to make:

model
schema 1.0

type user

type group
relations
define member: []

type folder
relations
define parent: []
define viewer: [] or viewer from parent

type document
relations
define parent: []
define viewer: []
define can_read: viewer or viewer from parent
[
// Bob is a member of the Sales group
{
"user": "user:bob",
"relation": "member",
"object": "group:sales",
},
// The "pricing" document is in "sales" folder
{
"user": "folder:sales",
"relation": "parent",
"object": "document:pricing",
},
// Members of the "sales" team can view the "sales" folder
{
"user": "group:sales#member",
"relation": "viewer",
"object": "folder:sales",
},
// John can view the "pricing" document
{
"user": "user:john",
"relation": "viewer",
"object": "document:pricing",
},
]

Those tuples match the intent of how the model was designed, but without type restrictions we can also write tuples that would not. For example, we can say that a document is a member of the sales group:

[
// The "pricing" document is a member of the "sales" group
{
"user": "document:pricing",
"relation": "member",
"object": "group:sales",
},
]

To be able to better validate tuples and make the model more readable, version 1.1 requires you to specify types for all the relations that were previously assignable (e.g. relations defined as self in any way), and it removes the as self keyword.

The model above needs to be rewritten as:

model
schema 1.1

type user

type group
relations
define member: [user]

type folder
relations
define parent: [folder]
define viewer: [user] or viewer from parent

type document
relations
define parent: [folder]
define viewer: [user]
define can_read: viewer or viewer from parent

After making these changes, Okta FGA will start validating the tuples more strictly, for example, you won’t be able to assign a document as a member of a group. If your application is writing invalid tuples, you’ll start getting errors when invoking the Write API.

Disallowing String Literals in user_ids

With version 1.0 models, you could write a tuple where the user id did not specify a type, for example:

[
// "bob" is a member of the "sales" group
{
"user": "bob",
"relation": "member",
"object": "group:sales",
},
]

However, with version 1.1 you always need to specify an object, so “bob’” is no longer a valid identifier. If you don’t have a type in your model that defines relations for users, you can add a ‘user’ type with no relations, for example:

model
schema 1.0

type user

You can then use that type when writing tuples:

[
// "bob" is a member of the "sales" group
{
"user": "user:bob",
"relation": "member",
"object": "group:sales",
},
]

Enforcing Userset Type Restrictions

With the model above, the following tuples will be valid according to the type definitions:

[
{
"user": "user:bob",
"relation": "member",
"object": "group:sales",
},
{
"user": "folder:sales",
"relation": "parent",
"object": "document:pricing",
},
{
"user": "user:john",
"relation": "viewer",
"object": "document:pricing",
},
]

However, the one below will not be valid, as we can’t assign group:sales#member to the viewer relationship of a folder.

[
{
"user": "group:sales#member",
"relation": "viewer",
"object": "folder:sales",
},
]

You might think that given group:sales#member are actually users, you should still be able to assign it. Okta FGA calls expressions like group:sales#member "usersets", and with our model we can only assign users.

The issue is that there are a lot of other usersets that you don't want to be assigned as viewers of a folder. For example, you would not want to add document:pricing#viewer as viewers of the folder as conceptually it does not make sense to say “every viewer of this document should be a viewer of this folder”.

To allow these tuples to be written, you need to specify group#member as a valid type for the folder’s viewer relationship. You would want to do the same with the document’s viewer relationship if you want to define that the members of a group can be viewers of a document:

model
schema 1.1

type user

type group
relations
define member: [user]

type folder
relations
define parent: [folder]
define viewer: [user, group#member] or viewer from parent

type document
relations
define parent: [folder]
define viewer: [user, group#member]
define can_read: viewer or viewer from parent

You can identify which usersets you need to add by looking at tuples in your store that have the following structure:

[
// Members of the "sales" group are viewers of the "sales" folder
{
"user": "group:sales#member",
"relation": "viewer",
"object": "folder:sales",
},
]

If you find a tuple like that, you’ll need to add group#member in the list of types allowed in the viewer relation of the folder type.

Public Access

When using version 1.0, you can indicate public access to specific objects by specifying a wildcard user in a relationship to any object, e.g.:

[
// All users are viewers of the "pricing" document
{
"user": "*",
"relation": "viewer",
"object": "document:pricing",
},
]

When you write the tuple above, all users are granted with the “viewer” relationship for the “pricing" document. You can write those kinds of tuples for any relation that is directly assignable in the model.

In version 1.1 we want to be more explicit about the tuples you can write, so you’ll need to declare in the DSL which relations allow wildcards and for which object types. If we want to let any object of type “user” to be a viewer of a specific document we’ll need to explicitly define it

model
schema 1.1

type user

type document
relations
define viewer: [user, user:*]

You’ll need to specify user:* as the user value in the tuple to enable this:

[
// All objects of type "user" are viewers of the "pricing" document
{
"user": "user:*",
"relation": "viewer",
"object": "document:pricing",
},
]

Being explicit about the wildcard type restrictions also lets you model scenarios like “all employees can see this document, but not all external users”, “all user accounts can access this document, but not service/machine-to-machine accounts”.

This change implies that you’ll need to change your code to write tuples with this new syntax, and that you’ll need to migrate existing tuples to use the new format.

You might have 3 kinds of tuples in your model that use “*”, with different migration strategies:

  1. Tuples that have user = “*”

You would need to retrieve those tuples and write them using the proper type (e.g. user:*). To retrieve them, you’ll need to use the Read endpoint, filter on your side the tuples that have user = “*”, and call the Write API for each one, with the proper type, e.g:

[
// All objects of type "user" are viewers of the "pricing" document
{
"user": "user:*",
"relation": "viewer",
"object": "document:pricing",
},
]
  1. Tuples that have user = "employee:*”, where employee is NOT a type that is defined in the new iteration of your model.

If you have tuples with this format, they will be considered invalid because they don’t have a corresponding type in the model. If you need such a type defined, you’ll need to add it to the model, and the scenario will be similar to the one described below.

  1. Tuples that have user = “user:*”, which would mean "the user with user_id = '*'”, where user is type that is defined in the new iteration of your model.

In this case, the meaning of the tuple will change. If you were intending to specify "a user with user id = *", you will need to encode it in a different way instead of using “*”. If you intended to specify “every user has this relationship with this object” then it’s not the way it would have worked with schema version = 1.0, but it will work with version = 1.1.

Query Evaluation Behavior with Type Restrictions

When you make changes to a model that already has tuples, those tuples might become invalid. Some cases where this can happen are:

  • If you rename/delete a type.
  • If you rename/delete a relation.
  • If you remove types from the list of allowed types in a relation, including changes for Public Access.
  • If Okta FGA introduces a change that makes a tuple invalid.

In these cases, Okta FGA will not consider those invalid tuples when evaluating queries (check, expand, list-objects, etc). However, after any of the changes above happens, you should delete those tuples as having a large number of invalid tuples will negatively affect performance.

Have Feedback?

Join us on the Discord community if you have any questions or suggestions.