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.
info

The model schema v1.0 has been deprecated. Migrate to schema v1.1 in order to be able to write tuples and run queries on your store.

A new DSL schema version has been introduced 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 will be:

  1. Adding model schema version field
  2. Adding type restrictions and removing need to specify as self
  3. Disallowing string literals in user_ids
  4. Enforcing type restrictions
  5. Requiring you to specify for which relations you can write tuples with public access
  6. Changes in query evaluation behavior with type restrictions

To facilitate migration to the new DSL schema, you will need to update tuples that are no longer valid. In particular, all tuples whose user field involves a wildcard character (* or user:*) defined with model schema 1.0 MUST be deleted and re-added back.

info

Before starting to migrate to the new model schema, it is recommended that you obtain your current authorization model ID and ensure that all your check, write, expand and list object are performed against that model id. This allows consistent behavior in your production system until you are ready to switch to the new model.

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
info

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

Type Restrictions & 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 as self
type folder
relations
define parent as self
define viewer as self or viewer from parent
type document
relations
define parent as self
define viewer as self
define can_read as viewer or viewer from parent
[// Bob is a member of the Sales group
{
"user": "user:bob",
"relation": "member",
"object": "group:sales",
"_description": "Bob is a member of the Sales group"
}// The "pricing" document is in "sales" folder
{
"user": "folder:sales",
"relation": "parent",
"object": "document:pricing",
"_description": "The \"pricing\" document is in \"sales\" folder"
}// Members of the "sales" team can view the "sales" folder
{
"user": "group:sales#member",
"relation": "viewer",
"object": "folder:sales",
"_description": "Members of the \"sales\" team can view the \"sales\" folder"
}// John can view the "pricing" document
{
"user": "user:john",
"relation": "viewer",
"object": "document:pricing",
"_description": "John can view the \"pricing\" document"
}]

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",
"_description": "The \"pricing\" document is a member of the \"sales\" group"
}]

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
{
"_description": "\"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 to your model, for example:

model
schema 1.1

type user

You can then use that type when writing tuples:

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

Enforcing 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",
"_description": "Members of the \"sales\" group are viewers of the \"sales\" folder"
}]

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",
"_description": "All users are viewers of the \"pricing\" document"
}]

When you write the tuple above, all users are granted 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",
"_description": "All objects of type \"user\" are viewers of the \"pricing\" document"
}]

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",
"_description": "All objects of type \"user\" are viewers of the \"pricing\" document"
}]
  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 public access to the object (e.g. "every user has this relationship with this object"), 1.0 models will not interpret the user:* value as a wildcard but 1.1 models will.

Warning

If you have any wildcard tuples (i.e., * or user:*) that were created with model schema 1.0, you must delete and re-add these tuples with the appropriate type. This allows Okta FGA to interpret these tuples appropriately with the model schema 1.1 semantics. Failure to delete and re-add may cause Okta FGA to interpret these tuples incorrectly.

Query Evaluation Behavior with Type Restrictions

It is possible to introduce new models and have existing tuples (from prior models) with incompatibilities with the new model. Some cases where this can happen include:

  • If you rename/delete a type.
  • If you rename/delete a relation.
  • If you remove a type restriction from the list of type restrictions for 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 happen, you should delete those tuples as having a large number of invalid tuples will negatively affect performance.

Improved Schema Validation

Type restrictions allow Okta FGA to validate the schema better at the time of writing the schema instead of at the time of query evaluation.

In Okta FGA, when referencing relations on related objects the relation tying the related objects (the word after from, also called the tupleset) cannot be evaluated - that means it cannot be referencing another relation, or allow non-concrete types (type bound public access (<object_type>:*) or usersets (<object_type>#<relation>)) in its type restrictions.

In schema 1.0, because type restrictions were not available, the validation error would occur at the time of evaluation (e.g. a Check call), while in schema 1.1, the error will be thrown when writing the model (during the WriteAuthorizationModel request).

In the case below, the write request will fail in the new schema 1.1 version, but would have succeeded in schema version 1.0:

model
schema 1.1
type user
type folder
relations
define parent: [folder,folder#parent]
define editor: [user] or editor from parent

/>

In schema 1.0, the WriteAuthorizationModel would have succeeded, but when attempting to write a tuple such as the one below, you would have received an error:


await fgaClient.write({
writes: [
{"user":"folder:product#parent","relation":"parent","object":"folder:planning"}
],
}, {
authorization_model_id: "1uHxCSuTP0VKPYSnkq1pbb1jeZw"
});

Deprecation Timeline

1- February 13th 2023: Deprecation Notice

  • This deprecation notice is posted.

2- March 31st 2023: Disallow writing new 1.0 models

3- May 1st 2023: Disallow evaluating 1.0 models

  • Query evaluations of any 1.0 models (Check, Expand, ListObjects and Write) will no longer be accepted.
  • ReadAuthorizationModel, ReadAuthorizationModels and ReadAssertions` will continue to serve results for 1.0 and 1.1 models.
Migrating Relations

Learn how to migrate relations

Have Feedback?

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