Conditions
Overview
Conditions allow you to model more complex authorization modeling scenarios involving attributes and can be used to represent some Attribute-based Access Control (ABAC) policies. Take a look at the Conditions and Conditional Relationship Tuples concepts for a quick overview.
There are various use cases where Conditions can be helpful. These include, but are not limited to:
- Temporal Access Policies - manage user access for a window of time.
- IP Allowlists or Geo-fencing Policies - limit or grant access based on an IP Address range or corporate network policy.
- Usage-based/Feature-based Policies (Entitlements) - enforce quota or usage of some resource or feature.
- Resource-attribute Policies - define policies to access resources based on attributes/fields of the resource(s).
For more information and background context on why we added this feature, please see our blog post on Conditional Relationship Tuples for OpenFGA.
Defining conditions in models
For this example we'll use the following authorization model to demonstrate a temporal based access policy. Namely, a user can view a document if and only if they have been granted the viewer relationship AND their non-expired grant policy is met.
model
schema 1.1
type user
type document
relations
define viewer: [user with non_expired_grant]
condition non_expired_grant(current_time: timestamp, grant_time: timestamp, grant_duration: duration) {
current_time < grant_time + grant_duration
}
The type restriction for document#viewer requires that any user of type user that is written in the relationship tuple must be accompanied by the non_expired_grant condition. This is denoted by the user with non_expired_grant specification.
Write the model to the FGA store:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const { authorization_model_id: id } = await fgaClient.writeAuthorizationModel({
"schema_version": "1.1",
"type_definitions": [
{
"type": "user"
},
{
"type": "document",
"relations": {
"viewer": {
"this": {}
}
},
"metadata": {
"relations": {
"viewer": {
"directly_related_user_types": [
{
"type": "user",
"condition": "non_expired_grant"
}
]
}
}
}
}
],
"conditions": {
"non_expired_grant": {
"name": "non_expired_grant",
"expression": "current_time < grant_time + grant_duration",
"parameters": {
"current_time": {
"type_name": "TYPE_NAME_TIMESTAMP"
},
"grant_duration": {
"type_name": "TYPE_NAME_DURATION"
},
"grant_time": {
"type_name": "TYPE_NAME_TIMESTAMP"
}
}
}
}
});
// id = "01HVMMBCMGZNT3SED4Z17ECXCA"
var writeAuthorizationModelRequestString = "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"viewer\":{\"this\":{}}},\"metadata\":{\"relations\":{\"viewer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"condition\":\"non_expired_grant\"}]}}}}],\"conditions\":{\"non_expired_grant\":{\"name\":\"non_expired_grant\",\"expression\":\"current_time < grant_time + grant_duration\",\"parameters\":{\"current_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"},\"grant_duration\":{\"type_name\":\"TYPE_NAME_DURATION\"},\"grant_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"}}}}}"
var body WriteAuthorizationModelRequest
if err := json.Unmarshal([]byte(writeAuthorizationModelRequestString), &body); err != nil {
// .. Handle error
return
}
data, err := fgaClient.WriteAuthorizationModel(context.Background()).
Body(body).
Execute()
if err != nil {
// .. Handle error
}
// data.AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA"
var modelJson = "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"viewer\":{\"this\":{}}},\"metadata\":{\"relations\":{\"viewer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"condition\":\"non_expired_grant\"}]}}}}],\"conditions\":{\"non_expired_grant\":{\"name\":\"non_expired_grant\",\"expression\":\"current_time < grant_time + grant_duration\",\"parameters\":{\"current_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"},\"grant_duration\":{\"type_name\":\"TYPE_NAME_DURATION\"},\"grant_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"}}}}}";
var body = JsonSerializer.Deserialize<OpenFga.Sdk.Model.WriteAuthorizationModelRequest>(modelJson);
var response = await fgaClient.WriteAuthorizationModel(body);
// response.AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA"
# from openfga_sdk.models.write_authorization_model_request import WriteAuthorizationModelRequest
async def write_authorization_model():
body_string = "{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"viewer\":{\"this\":{}}},\"metadata\":{\"relations\":{\"viewer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"condition\":\"non_expired_grant\"}]}}}}],\"conditions\":{\"non_expired_grant\":{\"name\":\"non_expired_grant\",\"expression\":\"current_time < grant_time + grant_duration\",\"parameters\":{\"current_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"},\"grant_duration\":{\"type_name\":\"TYPE_NAME_DURATION\"},\"grant_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"}}}}}"
response = await fga_client_instance.write_authorization_model(json.loads(body))
# response.authorization_model_id = "01HVMMBCMGZNT3SED4Z17ECXCA"
// import com.fasterxml.jackson.databind.ObjectMapper;
// import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest;
var mapper = new ObjectMapper().findAndRegisterModules();
var authorizationModel = fgaClient
.writeAuthorizationModel(mapper.readValue("{\"schema_version\":\"1.1\",\"type_definitions\":[{\"type\":\"user\"},{\"type\":\"document\",\"relations\":{\"viewer\":{\"this\":{}}},\"metadata\":{\"relations\":{\"viewer\":{\"directly_related_user_types\":[{\"type\":\"user\",\"condition\":\"non_expired_grant\"}]}}}}],\"conditions\":{\"non_expired_grant\":{\"name\":\"non_expired_grant\",\"expression\":\"current_time < grant_time + grant_duration\",\"parameters\":{\"current_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"},\"grant_duration\":{\"type_name\":\"TYPE_NAME_DURATION\"},\"grant_time\":{\"type_name\":\"TYPE_NAME_TIMESTAMP\"}}}}}", WriteAuthorizationModelRequest.class))
.get();
fga model write --store-id=${FGA_STORE_ID} --format=json '{"schema_version":"1.1","type_definitions":[{"type":"user"},{"type":"document","relations":{"viewer":{"this":{}}},"metadata":{"relations":{"viewer":{"directly_related_user_types":[{"type":"user","condition":"non_expired_grant"}]}}}}],"conditions":{"non_expired_grant":{"name":"non_expired_grant","expression":"current_time < grant_time + grant_duration","parameters":{"current_time":{"type_name":"TYPE_NAME_TIMESTAMP"},"grant_duration":{"type_name":"TYPE_NAME_DURATION"},"grant_time":{"type_name":"TYPE_NAME_TIMESTAMP"}}}}}'
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/authorization-models \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{"schema_version":"1.1","type_definitions":[{"type":"user"},{"type":"document","relations":{"viewer":{"this":{}}},"metadata":{"relations":{"viewer":{"directly_related_user_types":[{"type":"user","condition":"non_expired_grant"}]}}}}],"conditions":{"non_expired_grant":{"name":"non_expired_grant","expression":"current_time < grant_time + grant_duration","parameters":{"current_time":{"type_name":"TYPE_NAME_TIMESTAMP"},"grant_duration":{"type_name":"TYPE_NAME_DURATION"},"grant_time":{"type_name":"TYPE_NAME_TIMESTAMP"}}}}}'
Writing conditional relationship tuples
Using the model above, when we Write relationship tuples to the OpenFGA store, then any document#viewer relationship with user objects must be accompanied by the condition non_expired_grant because the type restriction requires it.
For example, we can give user:anne viewer access to document:1 for 10 minutes by writing the following relationship tuple:
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
const options = {
authorizationModelId: "01HVMMBCMGZNT3SED4Z17ECXCA",
};
await fgaClient.write({
writes: [
{"user":"user:anne","relation":"viewer","object":"document:1","condition":{"name":"non_expired_grant","context":{"grant_time":"2023-01-01T00:00:00Z","grant_duration":"10m"}}}
],
}, options);
options := ClientWriteOptions{
AuthorizationModelId: openfga.PtrString("01HVMMBCMGZNT3SED4Z17ECXCA"),
}
body := ClientWriteRequest{
Writes: []ClientTupleKey{
{
User: "user:anne",
Relation: "viewer",
Object: "document:1",
Condition: &RelationshipCondition{
Name: "non_expired_grant",
Context: &map[string]interface{}{"grant_time":"2023-01-01T00:00:00Z","grant_duration":"10m"},
},
},
},
}
data, err := fgaClient.Write(context.Background()).
Body(body).
Options(options).
Execute()
if err != nil {
// .. Handle error
}
_ = data // use the response
var options = new ClientWriteOptions {
AuthorizationModelId = "01HVMMBCMGZNT3SED4Z17ECXCA",
};
var body = new ClientWriteRequest() {
Writes = new List<ClientTupleKey>() {
new() {
User = "user:anne",
Relation = "viewer",
Object = "document:1",
Condition = new RelationshipCondition(){
Name = "non_expired_grant",
Context = new {
grant_time="2023-01-01T00:00:00Z",
grant_duration="10m"
}
}
}
},
};
var response = await fgaClient.Write(body, options);
options = {
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}
body = ClientWriteRequest(
writes=[
ClientTuple(
user="user:anne",
relation="viewer",
object="document:1",
condition=RelationshipCondition(
name='non_expired_grant',
context=dict(
grant_time="2023-01-01T00:00:00Z",
grant_duration="10m"
)
)
),
],
)
response = await fga_client.write(body, options)
var options = new ClientWriteOptions()
.authorizationModelId("01HVMMBCMGZNT3SED4Z17ECXCA");
var body = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
.user("user:anne")
.relation("viewer")
._object("document:1")
.condition(new ClientRelationshipCondition()
.name("non_expired_grant")
.context(Map.of("grant_time", "2023-01-01T00:00:00Z","grant_duration", "10m"))
)
));
var response = fgaClient.write(body, options).get();
fga tuple write --store-id=${FGA_STORE_ID} --model-id=01HVMMBCMGZNT3SED4Z17ECXCA user:anne viewer document:1 --condition-name non_expired_grant --condition-context '{"grant_time":"2023-01-01T00:00:00Z","grant_duration":"10m"}'
curl -X POST $FGA_API_URL/stores/$FGA_STORE_ID/write \
-H "Authorization: Bearer $FGA_API_TOKEN" \ # Not needed if service does not require authorization
-H "content-type: application/json" \
-d '{
"writes": {
"tuple_keys": [
{
"user": "user:anne",
"relation": "viewer",
"object": "document:1",
"condition": {
"name": "non_expired_grant",
"context": {
"grant_time": "2023-01-01T00:00:00Z",
"grant_duration": "10m"
}
}
}
]
},
"authorization_model_id": "01HVMMBCMGZNT3SED4Z17ECXCA"
}'
Queries with condition context
Now that we have written a Conditional Relationship Tuple, we can query OpenFGA using the Check API to see if user:anne has viewer access to document:1 under certain conditions/context. That is, user:anne should only have access if the current timestamp is less than the grant timestamp (e.g. the time which the tuple was written) plus the duration of the grant (10 minutes). If the current timestamp is less than, then you'll get a permissive decision. For example,
- Node.js
- Go
- .NET
- Python
- Java
- CLI
- curl
// Run a check
const { allowed } = await fgaClient.check({
user: 'user:anne',
relation: 'viewer',
object: 'document:1',
context: {"current_time":"2023-01-01T00:09:50Z"}