This is essentially a continuation of my previous post about openfga group sync
I have expanded it so that it doesn’t just sync groups, but also creates projects automatically per user.
As a quick overview, Authentik, an IDP/Single Sign On software has a feature called “expression policies”, which are essentially arbitrary python code. They are supposed to be used to check conditions, and then return true/false depending on whether or not you want to allow an event to happen. But, by always returning true, you can use them as hooks that call to external software (via http requests) and do stuff.
The first piece of the magic is an expression policy that syncs openfga groups to authentik groups.
import json
from authentik.core.models import Group
system_groups = ["authentik Read-only", "authentik Admins"]
all_groups = [group.name for group in Group.objects.all()]
all_groups = list(set(all_groups) - set(system_groups))
user_groups = [group.name for group in request.user.all_groups()]
openfga_api_key = ""
auth_model = ""
store_id = ""
writes = {
"writes": {
"tuple_keys": [
{
"user": f"user:{request.user.username}",
"relation": "member",
"object": "group"
}
]
},
"authorization_model_id": f"{auth_model}"
}
user_project_writes = {
"writes": {
"tuple_keys": [
{
"user": f"user:{request.user.username}",
"relation": "operator",
"object": f"project:{request.user.username}"
}
]
},
"authorization_model_id": f"{auth_model}"
}
deletes = {
"deletes": {
"tuple_keys": [
{
"user": f"user:{request.user.username}",
"relation": "member",
"object": "group"
}
]
},
"authorization_model_id": f"{auth_model}"
}
network_writes = {
"writes": {
"tuple_keys": [
{
"network": f"network:{request.user.username}-cloudnet",
"relation": "project",
"object": f"project:{request.user.username}"
}
]
},
"authorization_model_id": f"{auth_model}"
}
vlab_writes = {
"writes": {
"tuple_keys": [
{
"network": f"network:{request.user.username}-vlab",
"relation": "project",
"object": f"project:{request.user.username}"
}
]
},
"authorization_model_id": f"{auth_model}"
}
headers = {
"Authorization": f"Bearer {openfga_api_key}",
"content-type": "application/json"
}
for group in list(set(all_groups) - set(user_groups)):
deletes["deletes"]["tuple_keys"][0]["object"] = f"group:{group}"
requests.post(f"https://openfga.moonpiedumpl.ing/stores/{store_id}/write", data=json.dumps(deletes), headers=headers)
requests.post(f"https://openfga.moonpiedumpl.ing/stores/{store_id}/write", data=json.dumps(vlab_writes), headers=headers)
requests.post(f"https://openfga.moonpiedumpl.ing/stores/{store_id}/write", data=json.dumps(network_writes), headers=headers)
requests.post(f"https://openfga.moonpiedumpl.ing/stores/{store_id}/write", data=json.dumps(user_project_writes), headers=headers)
for group in user_groups:
writes["writes"]["tuple_keys"][0]["object"] = f"group:{group}"
requests.post(f"https://openfga.moonpiedumpl.ing/stores/{store_id}/write", data=json.dumps(writes), headers=headers)
return True
The second part of the magic is an authentik expression policy that creates Incus projects per user:
import requests
import tempfile
import os
import warnings
warnings.filterwarnings("ignore")
# ====== CONFIG ======
INCUS_API = "ip.ip.ip.ip:8443"
# Desired project state (used for create/update)
PROJECT_PAYLOAD = {
"name": request.user.username,
"description": "User specific project",
"config": {
"features.images": "false",
"features.networks": "true",
"features.storage.buckets": "true",
"features.profiles": "true",
"features.storage.volumes": "true",
"limits.cpu": 4,
"limits.memory": "8GiB",
"restricted": "true",
"restricted.containers.nesting": "allow",
"restricted.backups": "block",
"restricted.snapshots": "allow",
"restricted.networks.uplinks": "forovn0"
}
}
cert_path = "/incus-secrets/incus-crt"
key_path = "/incus-secrets/incus-key"
s = requests.Session()
s.verify = False
s.cert = (cert_path, key_path)
# 1) Check if the project exists
get_url = f"{INCUS_API}/1.0/projects/{request.user.username}"
r = s.get(get_url)
if r.status_code == 200:
# 2a) Exists -> PUT update
put_url = get_url
s.put(put_url, json=PROJECT_PAYLOAD)
elif r.status_code == 404:
# 2b) Missing -> POST create
post_url = f"{INCUS_API}/1.0/projects"
s.post(post_url, json=PROJECT_PAYLOAD)
return True
To get the cert used, I mount it into the authentik server container. This is important because it’s the server container that executes expression policies. Here is a snippet of the helm release:
I did have to seperate the incus pfx cert out into two seperate files before mounting it in:
openssl pkcs12 -in incus-ui.pfx -nocerts -nodes -out client.key
openssl pkcs12 -in incus-ui.pfx -clcerts -nokeys -out client.crt
And then I put this into a kubernetes secret and mount it in via helm.
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: authentik
namespace: authentik
spec:
chart:
spec:
chart: authentik
reconcileStrategy: ChartVersion
sourceRef:
kind: HelmRepository
name: authentik
interval: 1m0s
values:
server:
volumes:
- name: incus-creds
secret:
secretName: incus-secrets
volumeMounts:
- name: incus-creds
mountPath: /incus-secrets
worker:
volumes:
- name: incus-creds
secret:
secretName: incus-secrets
volumeMounts:
- name: incus-creds
mountPath: /incus-secrets
readOnly: true
Of course it is different depending on if you are using helm and values directly, or docker or podman. But you should get the idea.
And with this, users automatically get their own private projects, with limited resources. Another benefit is that I can update the configs of these projects and make them apply again on login, so if I wanted to do things like retroactively allow access to the GPU, or increase/decrease resources, I could do that.
For networks, users can create their own private networks by using OVN and forovn0 as an uplink. I figured that one out in this post.
One thing I have done, to avoid a potential security issue, is to create and then lock Authentik users with names that have the same names as the existing projects. This prevents people from creating an account with a username of “Default” and then getting access to the “Default” Incus project.