Migrating Models To Schema 1.1
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.
In short, we’ll be:
- Adding type restrictions.
- Removing the need to specify
as self
, and - 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
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:
- DSL
- JSON
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
{
"type_definitions": [
{
"type": "user",
"relations": {}
},
{
"type": "group",
"relations": {
"member": {
"this": {}
}
}
},
{
"type": "folder",
"relations": {
"parent": {
"this": {}
},
"viewer": {
"union": {
"child": [
{
"this": {}
},
{
"tupleToUserset": {
"tupleset": {
"object": "",
"relation": "parent"
},
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
]
}
}
}
},
{
"type": "document",
"relations": {
"parent": {
"this": {}
},
"viewer": {
"this": {}
},
"can_read": {
"union": {
"child": [
{
"computedUserset": {
"object": "",
"relation": "viewer"
}
},
{
"tupleToUserset": {
"tupleset": {
"object": "",
"relation": "parent"
},
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
]
}
}
}
}
],
"schema_version": "1.0"
}
[
// 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:
- DSL
- JSON
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
{
"type_definitions": [
{
"type": "user",
"relations": {}
},
{
"type": "group",
"relations": {
"member": {
"this": {}
}
},
"metadata": {
"relations": {
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
},
{
"type": "folder",
"relations": {
"parent": {
"this": {}
},
"viewer": {
"union": {
"child": [
{
"this": {}
},
{
"tupleToUserset": {
"tupleset": {
"object": "",
"relation": "parent"
},
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
]
}
}
},
"metadata": {
"relations": {
"parent": {
"directly_related_user_types": [
{
"type": "folder"
}
]
},
"viewer": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
},
{
"type": "document",
"relations": {
"parent": {
"this": {}
},
"viewer": {
"this": {}
},
"can_read": {
"union": {
"child": [
{
"computedUserset": {
"object": "",
"relation": "viewer"
}
},
{
"tupleToUserset": {
"tupleset": {
"object": "",
"relation": "parent"
},
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
]
}
}
},
"metadata": {
"relations": {
"parent": {
"directly_related_user_types": [
{
"type": "folder"
}
]
},
"viewer": {
"directly_related_user_types": [
{
"type": "user"
}
]
},
"can_read": {
"directly_related_user_types": []
}
}
}
}
],
"schema_version": "1.1"
}
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:
- DSL
- JSON
model
schema 1.0
type user
{
"type_definitions": [
{
"type": "user",
"relations": {}
}
],
"schema_version": "1.0"
}
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:
- DSL
- JSON
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
{
"type_definitions": [
{
"type": "user",
"relations": {}
},
{
"type": "group",
"relations": {
"member": {
"this": {}
}
},
"metadata": {
"relations": {
"member": {
"directly_related_user_types": [
{
"type": "user"
}
]
}
}
}
},
{
"type": "folder",
"relations": {
"parent": {
"this": {}
},
"viewer": {
"union": {
"child": [
{
"this": {}
},
{
"tupleToUserset": {
"tupleset": {
"object": "",
"relation": "parent"
},
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
]
}
}
},
"metadata": {
"relations": {
"parent": {
"directly_related_user_types": [
{
"type": "folder"
}
]
},
"viewer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "group",
"relation": "member"
}
]
}
}
}
},
{
"type": "document",
"relations": {
"parent": {
"this": {}
},
"viewer": {
"this": {}
},
"can_read": {
"union": {
"child": [
{
"computedUserset": {
"object": "",
"relation": "viewer"
}
},
{
"tupleToUserset": {
"tupleset": {
"object": "",
"relation": "parent"
},
"computedUserset": {
"object": "",
"relation": "viewer"
}
}
}
]
}
}
},
"metadata": {
"relations": {
"parent": {
"directly_related_user_types": [
{
"type": "folder"
}
]
},
"viewer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "group",
"relation": "member"
}
]
},
"can_read": {
"directly_related_user_types": []
}
}
}
}
],
"schema_version": "1.1"
}
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
- DSL
- JSON
model
schema 1.1
type user
type document
relations
define viewer: [user, user:*]
{
"type_definitions": [
{
"type": "user",
"relations": {}
},
{
"type": "document",
"relations": {
"viewer": {
"this": {}
}
},
"metadata": {
"relations": {
"viewer": {
"directly_related_user_types": [
{
"type": "user"
},
{
"type": "user",
"wildcard": {}
}
]
}
}
}
}
],
"schema_version": "1.1"
}
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:
- 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",
},
]
- Tuples that have
user = "employee:*”
, whereemployee
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.
- Tuples that have
user = “user:*”
, which would mean "the user with user_id = '*'”, whereuser
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.