The Google Drive app starts and a moment later your files appear. It's magic. But have you ever wondered what's your files actually? How do these services actually know, which files you are allowed to see?
Are you part of an Organization and you are allowed to view all their files? Have you been assigned to a Team, that's allowed to view or edit files? Has someone shared their files with you as a User?
So in 2019 Google has lifted the curtain and has published a paper on Google Zanzibar, which is Google's central solution for providing authorization among its many services:
The keyword here is Relationship-based Access Control, which is ...
[...] an authorization paradigm where a subject's permission to access a resource is defined by the presence of relationships between those subjects and resources.
I have previously written an article about the Google Zanzibar Data Model, and also wrote some pretty nice SQL statements to make sense of the it. This repository implements Relationship-based Access Control using ASP.NET Core, EntityFramework Core and Microsoft SQL Server.
This repository is an OData-enabled implementation of the RESTful API implementing ReBAC in ASP.NET Core:
The blog article for this repository can be found at:
We got everything in place. We can now start the application and use Swagger to query it. But Visual Studio 2022 now comes with the "Endpoints Explorer" to execute HTTP Requests against HTTP endpoints. Though it's not fully-fledged yet, I think it'll improve with time and it already covers a lot of use cases.
You can find the Endpoints Explorer at:
View -> Other Windows -> Endpoints Explorer
By clicking on RebacExperiments.Server.Api.http
the HTTP script with the sample requests comes up.
We have got 2 Tasks:
task_152
: "Sign Document"task 323
: "Call Back Philipp Wagner"
And we have got two users:
user_philipp
: "Philipp Wagner"user_max
: "Max Mustermann"
Both users are permitted to login, so they are allowed to query for data, given a permitted role and permissions.
There are two Organizations:
- Organization 1: "Organization #1"
- Organization 2: "Organization #2"
And 2 Roles:
role_user
: "User" (Allowed to Query for UserTasks)role_admin
: "Administrator" (Allowed to Delete a UserTask)
The Relationships between the entities are the following:
The Relationship-Table is given below.
ObjectKey | ObjectNamespace | ObjectRelation | SubjectKey | SubjectNamespace | SubjectRelation
--------------------|-------------------|-------------------|-----------------------|-----------------------|-------------------
:task_323 : | UserTask | viewer | :organization_1: | Organization | member
:task_152 : | UserTask | viewer | :organization_1: | Organization | member
:task_152 : | UserTask | viewer | :organization_2: | Organization | member
:organization_1: | Organization | member | :user_philipp: | User | NULL
:organization_2: | Organization | member | :user_max: | User | NULL
:role_user: | Role | member | :user_philipp: | User | NULL
:role_admin: | Role | member | :user_philipp: | User | NULL
:role_user: | Role | member | :user_max: | User | NULL
:task_323: | UserTask | owner | :user_2: | User | member
We can draw the following conclusions here: A member
of organization_1
is viewer
of task_152
and task_323
. A member
of organization_2
is a viewer
of task_152
only. user_philipp
is member of organization_1
, so the user is able to see
both tasks as viewer
. user_max
is member of organization_2
, so he is a viewer
of task_152
only. user_philipp
has the
User
and Administrator
roles assigned, so he can create, query and delete a UserTask
. user_max
only has the User
role
assigned, so he is not authorized to delete a UserTask
. Finally user_philipp
is also the owner
of task_323
so he is
permitted to update the data of the UserTask
.
We start by defining the Host Address:
@RebacExperiments.Server.Api_HostAddress = https://localhost:5000/odata
Then we signin [email protected]
using the SignInUser
Action:
### Sign In "[email protected]"
POST {{RebacExperiments.Server.Api_HostAddress}}/SignInUser
Content-Type: application/json
{
"username": "[email protected]",
"password": "5!F25GbKwU3P",
"rememberMe": true
}
And then we get all UserTask
entities for the current user:
### Get all UserTasks for "[email protected]"
GET {{RebacExperiments.Server.Api_HostAddress}}/UserTasks
The response is going to contain two entities:
{
"@odata.context": "https://localhost:5000/odata/$metadata#UserTasks",
"value": [
{
"title": "Call Back",
"description": "Call Back Philipp Wagner",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Low",
"userTaskStatus": "NotStarted",
"id": 152,
"rowVersion": "AAAAAAAAB\u002Bw=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
},
{
"title": "Sign Document",
"description": "You need to Sign a Document",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Normal",
"userTaskStatus": "InProgress",
"id": 323,
"rowVersion": "AAAAAAAAB\u002B0=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
}
]
}
We can then introduce some OData Goodies and say we want only 1
entity, the results should be
ordered by the id
property and the response should contain the total number of entities the user
is authorized to acces.
### Get the first task and return the total count of Entities visible to "[email protected]"
GET {{RebacExperiments.Server.Api_HostAddress}}/UserTasks?$top=1&$orderby=id&$count=true
The result is going to contain the @odata.count
property and have 1
task only.
{
"@odata.context": "https://localhost:5000/odata/$metadata#UserTasks",
"@odata.count": 2,
"value": [
{
"title": "Call Back",
"description": "Call Back Philipp Wagner",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Low",
"userTaskStatus": "NotStarted",
"id": 152,
"rowVersion": "AAAAAAAAB\u002Bw=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
}
]
}
We can then sign in as [email protected]
.
### Sign In as "[email protected]"
POST {{RebacExperiments.Server.Api_HostAddress}}/SignInUser
Content-Type: application/json
{
"username": "[email protected]",
"password": "5!F25GbKwU3P",
"rememberMe": true
}
If you try to get all UserTask
entities of [email protected]
:
### Get all UserTasks for "[email protected]"
GET {{RebacExperiments.Server.Api_HostAddress}}/UserTasks
There will be 1
task only.
{
"@odata.context": "https://localhost:5000/odata/$metadata#UserTasks",
"value": [
{
"title": "Call Back",
"description": "Call Back Philipp Wagner",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Low",
"userTaskStatus": "NotStarted",
"id": 152,
"rowVersion": "AAAAAAAAB\u002Bw=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
}
]
}
Now we'll create a new UserTask
"API HTTP File Example":
### Create a new UserTask "API HTTP File Example" as "[email protected]"
POST {{RebacExperiments.Server.Api_HostAddress}}/UserTasks
Content-Type: application/json
{
"title": "API HTTP File Example",
"description": "API HTTP File Example",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Normal",
"userTaskStatus": "NotStarted"
}
And we can see, that [email protected]
now sees both UserTask
entities:
{
"@odata.context": "https://localhost:5000/odata/$metadata#UserTasks",
"value": [
{
"title": "Call Back",
"description": "Call Back Philipp Wagner",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Low",
"userTaskStatus": "NotStarted",
"id": 152,
"rowVersion": "AAAAAAAAB\u002Bw=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
},
{
"title": "API HTTP File Example",
"description": "API HTTP File Example",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Normal",
"userTaskStatus": "NotStarted",
"id": 38191,
"rowVersion": "AAAAAAAACAY=",
"lastEditedBy": 7,
"validFrom": "2023-10-25T19:58:44.8007138\u002B02:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
}
]
}
If we sign in as [email protected]
again:
### Sign In "[email protected]"
POST {{RebacExperiments.Server.Api_HostAddress}}/SignInUser
Content-Type: application/json
{
"username": "[email protected]",
"password": "5!F25GbKwU3P",
"rememberMe": true
}
We can see with a call to /UserTasks
, that he doesn't see the new UserTask
at all.
{
"@odata.context": "https://localhost:5000/odata/$metadata#UserTasks",
"value": [
{
"title": "Call Back",
"description": "Call Back Philipp Wagner",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Low",
"userTaskStatus": "NotStarted",
"id": 152,
"rowVersion": "AAAAAAAAB\u002Bw=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
},
{
"title": "Sign Document",
"description": "You need to Sign a Document",
"dueDateTime": null,
"reminderDateTime": null,
"completedDateTime": null,
"assignedTo": null,
"userTaskPriority": "Normal",
"userTaskStatus": "InProgress",
"id": 323,
"rowVersion": "AAAAAAAAB\u002B0=",
"lastEditedBy": 1,
"validFrom": "2013-01-01T00:00:00\u002B01:00",
"validTo": "9999-12-31T23:59:59.9999999\u002B01:00"
}
]
}