|
This is part of AIP-1, which aims to run Airflow in a multi-tenant way. Today, Airflow doesn’t have built-in support for multi-tenant environments. Airflow provides Role-Based Access Control (RBAC), but the access control is not fine-grained and allows various Airflow components and users to access/modify resources across the environment without restriction. This document describes a proposal to introduce a tenant model paving a way to running Airflow in a multi-tenant fashion.
Note: The resource access control is out of scope for this AIP. It will be done in a separate AIP(s). The purpose of this AIP is to introduce a new “tenant” entity that will be utilized in the future AIPs related to multi tenancy.
The current access management uses a role based access control (RBAC) to authorize users to access specific pages/resources. Here is the database diagram.

To better understand it, here is an example. This is only a simplified version of an Airflow configuration.

The proposal is to introduce a new entity called "tenant". Users can belong to one or multiple tenants. The purpose of a tenant is to be self isolated from each other, therefore, users in a tenant cannot have access to users and roles from another tenant (unless they have Admin permissions). Note: the resource access control is out of scope for this AIP. It will be done in a separate AIP(s).
A new Airflow configuration is introduced to whether activate this new entity "tenant":
[core] enable_tenantWhen enabling this option, the multi tenancy experience will be enabled, as such Admins will be able to manage tenants and to assign users to them. On users side, they will be then restricted to their tenant scope(s). In other words, if a user has "List users" permissions, when enabling this option, this user will be able to list only users which belongs to the same tenants he belongs to as opposed to all users when the option was turned off.
Since some database changes are needed to introduce this new tenant entity (see next section), and by default tenants will be disabled, we need the database to be compatible with when [core] enable_tenant is True or False. TODO: need to explain global default tenant here

To better understand it, here is an example:

---
In order to manage the new entity tenant, some modifications have to be made across the UI, the Rest API and the Airflow CLI. Airflow CLI changes will be described in its own section at the end of this section.
![]()

Input
GET /tenants
| Query parameters | |||
|---|---|---|---|
| Name | Type | Default | Description |
| limit | integer | 100 | The numbers of items to return. |
| offset | integer | 0 | The number of items to skip before starting to collect the result set. |
| order_by | string | The name of the field to order the results by. Prefix a field name with | |
{
"tenants": [
{
"name": "string"
}
],
"total_entries": 0
} |
| Code | Description | Schema |
|---|---|---|
| 401 | Request not authenticated due to missing, invalid, authentication info. | Similar to get_roles API |
| 403 | Client does not have sufficient permission. | Similar to get_roles API |

Input
POST /tenants
| Body schema | ||
|---|---|---|
| Name | Type | Description |
| name | string | The tenant name |
{
"name": "string"
} |
| Code | Description | Schema |
|---|---|---|
| 400 | Client specified an invalid argument. | Similar to post_role API |
| 401 | Request not authenticated due to missing, invalid, authentication info. | Similar to post_role API |
| 403 | Client does not have sufficient permission. | Similar to post_role API |
Get tenant

GET /tenants/{tenant_name}| Path parameters | ||
|---|---|---|
| Name | Type | Description |
| tenant_name | string | The tenant name |
{
"name": "string"
} |
| Code | Description | Schema |
|---|---|---|
| 401 | Request not authenticated due to missing, invalid, authentication info. | Similar to get_role API |
| 403 | Client does not have sufficient permission. | Similar to get_role API |
| 404 | A specified resource is not found. | Similar to get_role API |

PATCH /tenants/{tenant_name}| Path parameters | ||
|---|---|---|
| Name | Type | Description |
| tenant_name | string | The tenant name |
| Body schema | ||
| name | string | The tenant name |
{
"name": "string"
} |
| Code | Description | Schema |
|---|---|---|
| 400 | Client specified an invalid argument. | Similar to patch_role API |
| 401 | Request not authenticated due to missing, invalid, authentication info. | Similar to patch_role API |
| 403 | Client does not have sufficient permission. | Similar to patch_role API |
| 404 | A specified resource is not found. | Similar to patch_role API |

DELETE /tenants/{tenant_name}| Path parameters | ||
|---|---|---|
| Name | Type | Description |
| tenant_name | string | The tenant name |
No output
| Code | Description | Schema |
|---|---|---|
| 400 | Client specified an invalid argument. | Similar to delete_role API |
| 401 | Request not authenticated due to missing, invalid, authentication info. | Similar to delete_role API |
| 403 | Client does not have sufficient permission. | Similar to delete_role API |
| 404 | A specified resource is not found. | Similar to delete_role API |
For each role, the list of associated tenants needs to be added.
Only roles which are associated to the same tenant(s) as the logged in user need to be returned.
Open question: What if Admin is associated to HR and Marketing tenants and the logged in user has permission to list roles in the HR tenant. Should HR be the only tenant returned as part of Admin's associated tenants information?

Only the output has to be updated. The field "tenants"needs to be added.
{
"roles": [
{
"name": "string",
"actions": [
{
"action": {
"name": "string"
},
"resource": {
"name": "string"
}
}
],
"tenants": [
{
"name": "string",
}
]
}
],
"total_entries": 0
} |
The list of associated tenants needs to be added as mandatory field.

POST /roles
| Body schema | |||
|---|---|---|---|
| Name | Type | Description | Comments |
| tenants | array of objects | List of tenants associated to the role | To be added |
{
"name": "string",
"actions": [
{
"action": {
"name": "string"
},
"resource": {
"name": "string"
}
}
],
"tenants": [
{
"name": "string",
}
]
} |
{
"name": "string",
"actions": [
{
"action": {
"name": "string"
},
"resource": {
"name": "string"
}
}
],
"tenants": [
{
"name": "string",
}
]
} |
Unchanged
The list of associated tenants needs to be added.
Only if the role is associated to the same tenant(s) as the logged in user can be returned.
Open question: What if Admin is associated to HR and Marketing tenants and the logged in user has permission to read roles in the HR tenant. Should HR be the only tenant returned as part of Admin's associated tenants information?

Only the output has to be updated. The field "tenants"needs to be added.
{
"name": "string",
"actions": [
{
"action": {
"name": "string"
},
"resource": {
"name": "string"
}
}
],
"tenants": [
{
"name": "string",
}
]
} |
The list of associated tenants needs to be added.
Only if the role is associated to the same tenant(s) as the logged in user can be modified.
Open question: What if Admin is associated to HR and Marketing tenants and the logged in user has permission to edit roles in the HR tenant. Should the update request be applied to only tenants the logged in user has permissions to? In other words, in this example, if the logged in user edit the role Admin and remove HR from the tenants list, it will send an empty list as tenants associated to the role but as consequence will just remove HR from the association (hence keeping Marketing as tenant associated to the role Admin)?

PATCH /roles/{role_name}| Query parameters | |||
|---|---|---|---|
| Name | Type | Description | Comments |
| update_mask | Array of strings | The fields to update on the resource. If absent or empty, all modifiable fields are updated. A comma-separated list of fully qualified names of fields. | Need to handle the new field "tenants" |
| Body schema | |||
| Name | Type | Description | Comments |
| tenants | array of objects | List of tenants associated to the role | To be added |
{
"name": "string",
"actions": [
{
"action": {
"name": "string"
},
"resource": {
"name": "string"
}
}
],
"tenants": [
{
"name": "string",
}
]
} |
{
"name": "string",
"actions": [
{
"action": {
"name": "string"
},
"resource": {
"name": "string"
}
}
],
"tenants": [
{
"name": "string",
}
]
} |
Unchanged
For each user, the associated role needs to be updated to the associated “Role in Tenant”.
Only users which belong to the same tenant(s) as the logged in user need to be returned.
Open question: What if John belongs to HR and Marketing tenants and the logged in user has permission to list users in the HR tenant. Should HR be the only tenant returned as part of John’s "role in tenant" information?

Only the output has to be updated. The field "roles" needs to be replaced by "tenant_roles".
{
"users": [
{
"first_name": "string",
"last_name": "string",
"username": "string",
"email": "string",
"active": true,
"last_login": "string",
"login_count": 0,
"failed_login_count": 0,
"tenant_roles": [
{
"role": {
"name": "string"
},
"tenant": {
"name": "string"
}
}
],
"created_on": "string",
"changed_on": "string"
}
],
"total_entries": 0
} |
The associated role needs to be updated to the associated “Role in Tenant”.

POST /users
| Body schema | |||
|---|---|---|---|
| Name | Type | Description | Comments |
| roles | Array of objects | User roles | To be removed |
| tenant_roles | Array of objects | User tenant roles | To be added |
{
"first_name": "string",
"last_name": "string",
"username": "string",
"email": "string",
"tenant_roles": [
{
"role": {
"name": "string"
},
"tenant": {
"name": "string"
}
}
]
} |
{
"first_name": "string",
"last_name": "string",
"username": "string",
"email": "string",
"active": true,
"last_login": "string",
"login_count": 0,
"failed_login_count": 0,
"tenant_roles": [
{
"role": {
"name": "string"
},
"tenant": {
"name": "string"
}
}
],
"created_on": "string",
"changed_on": "string"
} |
Unchanged
The associated role needs to be updated to the associated “Role in Tenant”.
Only if the user belongs to the same tenant(s) as the logged in user need can be returned.
Open question: What if John belongs to HR and Marketing tenants and the logged in user has permission to read users in the HR tenant. Should HR be the only tenant returned as part of John’s "role in tenant" information?

Only the output has to be updated. The field "roles" needs to be replaced by "tenant_roles".
{
"first_name": "string",
"last_name": "string",
"username": "string",
"email": "string",
"active": true,
"last_login": "string",
"login_count": 0,
"failed_login_count": 0,
"tenant_roles": [
{
"role": {
"name": "string"
},
"tenant": {
"name": "string"
}
}
],
"created_on": "string",
"changed_on": "string"
} |
The associated role needs to be updated to the associated “Role in Tenant”.
Only if the user belongs to the same tenant(s) as the logged in user need can be modified.
Open question: What if John belongs to HR and Marketing tenants and the logged in user has permission to edit users in the HR tenant. Should the update request be applied to only tenants the logged in user has permissions to? In other words, in this example, if the logged in user edit the user John and remove "Admin in HR" from the list, it will send an empty list as "role in tenant" associated to the user but as consequence will just remove "Admin in HR" from the association (hence keeping "Admin in Marketing" as "role in tenant" associated to the user John)?

PATCH /users/{username}| Query parameters | |||
|---|---|---|---|
| Name | Type | Description | Comments |
| update_mask | Array of strings | The fields to update on the resource. If absent or empty, all modifiable fields are updated. A comma-separated list of fully qualified names of fields. | Need to handle the new field "tenant_roles" |
| Body schema | |||
| Name | Type | Description | Comments |
| roles | Array of objects | User roles | To be removed |
| tenant_roles | Array of objects | User tenant roles | To be added |
{
"first_name": "string",
"last_name": "string",
"username": "string",
"email": "string",
"tenant_roles": [
{
"role": {
"name": "string"
},
"tenant": {
"name": "string"
}
}
]
} |
{
"first_name": "string",
"last_name": "string",
"username": "string",
"email": "string",
"active": true,
"last_login": "string",
"login_count": 0,
"failed_login_count": 0,
"tenant_roles": [
{
"role": {
"name": "string"
},
"tenant": {
"name": "string"
}
}
],
"created_on": "string",
"changed_on": "string"
} |
Unchanged
A new sub-command needs to be created: "tenants". Below are the commands associated to this new sub-command.
airflow tenants [-h] COMMAND ...
| Command | Description | Arguments |
|---|---|---|
| create | Create tenant |
|
| delete | Delete tenant |
|
export | Export tenants from db to JSON file | Similar to export roles |
| import | Import tenants from JSON file to db | Similar to import roles |
| list | List tenants |
|
| Command | Description | Arguments | Comments |
|---|---|---|---|
| add-tenant | Associate a tenant to the role |
| To be added |
| del-tenant | Dissociate a tenant from the role |
| To be added |
| create |
| Need to add --tenant as mandatory argument. To keep it simple, associate only one tenant at creation. To add more, the user can use "add-tenant". | |
| export | Need to export tenants | ||
| import | Need to import tenants | ||
| list | Need to return tenants |
| Command | Description | Arguments | Comments |
|---|---|---|---|
| add-role | To be removed | ||
| remove-role | To be removed | ||
| add-role-tenant | Add a role associated to a tenant to a user |
| To be added |
| remove-role-tenant | Remove a role associated to a tenant to a user |
| To be added |
| create |
| Need to add the argument --tenant | |
| export | Need to export role tenants | ||
| import | Need to import role tenants | ||
| list | Need to return role tenants |
Permissions
The following permissions need to be added in order to grant/deny access to tenants management.
Action | Resource |
can_create | Tenant |
can_delete | Tenant |
can_edit | Tenant |
can_read | Tenant |
menu_access | List Tenants |
A new Airflow configuration is introduced to whether activate this new entity "tenants":
[core] enable_tenantsWhen enabling this option, if there is no tenant already configured, a default tenant configuration will be created. This initial configuration needs to be created so that the transition between being non multi tenancy and being tenancy is as seamless as possible.
A POC will be created in order to verify that the new tenant entity and modified relationship using existing FlaskApp Builder actually works. This POC will be done before AIP voting.
This change introduce this new entity "tenant" which allows administrators/operators to group users in different groups.
Only those that set [core]enable_tenants=True
Users can be organized in tenants by administrators/operators.