Skip to content

Commit

Permalink
Merge branch 'main' into web/bad-default-in-select
Browse files Browse the repository at this point in the history
* main:
  translate: Updates for file web/xliff/en.xlf in fr (#8272)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#8271)
  fix codeowners
  core: bump goauthentik.io/api/v3 from 3.2023106.3 to 3.2023106.4
  web: bump API Client version (#8269)
  root: Multi-tenancy (#7590)
  website/docs: add helm chart 2024.1 breaking changes
  core: fix rac property mapping requiring enterprise (#8267)
  web: bump the wdio group in /tests/wdio with 4 updates (#8262)
  web: bump the eslint group in /tests/wdio with 2 updates (#8261)
  web: bump the eslint group in /web with 2 updates (#8263)
  core: bump uvicorn from 0.26.0 to 0.27.0 (#8264)
  • Loading branch information
kensternberg-authentik committed Jan 23, 2024
2 parents 77e654c + 862aece commit 780d1a2
Show file tree
Hide file tree
Showing 236 changed files with 8,080 additions and 3,745 deletions.
2 changes: 2 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ scripts/ @goauthentik/backend
tests/ @goauthentik/backend
pyproject.toml @goauthentik/backend
poetry.lock @goauthentik/backend
go.mod @goauthentik/backend
go.sum @goauthentik/backend
# Infrastructure
.github/ @goauthentik/infrastructure
Dockerfile @goauthentik/infrastructure
Expand Down
24 changes: 17 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,14 @@ dev-reset: dev-drop-db dev-create-db migrate ## Drop and restore the Authentik
#########################

gen-build: ## Extract the schema from the database
AUTHENTIK_DEBUG=true ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true ak spectacular --file schema.yml
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
ak make_blueprint_schema > blueprints/schema.json
AUTHENTIK_DEBUG=true \
AUTHENTIK_TENANTS__ENABLED=true \
AUTHENTIK_OUTPOSTS__DISABLE_EMBEDDED_OUTPOST=true \
ak spectacular --file schema.yml

gen-changelog: ## (Release) generate the changelog based from the commits since the last tag
git log --pretty=format:" - %s" $(shell git describe --tags $(shell git rev-list --tags --max-count=1))...$(shell git branch --show-current) | sort > changelog.md
Expand All @@ -116,12 +122,16 @@ gen-diff: ## (Release) generate the changelog diff between the current schema a
sed -i 's/}/}/g' diff.md
npx prettier --write diff.md

gen-clean:
rm -rf gen-go-api/
gen-clean-ts: ## Remove generated API client for Typescript
rm -rf gen-ts-api/
rm -rf web/node_modules/@goauthentik/api/

gen-client-ts: ## Build and install the authentik API for Typescript into the authentik UI Application
gen-clean-go: ## Remove generated API client for Go
rm -rf gen-go-api/

gen-clean: gen-clean-ts gen-clean-go ## Remove generated API clients

gen-client-ts: gen-clean-ts ## Build and install the authentik API for Typescript into the authentik UI Application
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
Expand All @@ -137,7 +147,7 @@ gen-client-ts: ## Build and install the authentik API for Typescript into the a
cd gen-ts-api && npm i
\cp -rfv gen-ts-api/* web/node_modules/@goauthentik/api

gen-client-go: ## Build and install the authentik API for Golang
gen-client-go: gen-clean-go ## Build and install the authentik API for Golang
mkdir -p ./gen-go-api ./gen-go-api/templates
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O ./gen-go-api/config.yaml
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O ./gen-go-api/templates/README.mustache
Expand All @@ -157,7 +167,7 @@ gen-client-go: ## Build and install the authentik API for Golang
gen-dev-config: ## Generate a local development config file
python -m scripts.generate_config

gen: gen-build gen-clean gen-client-ts
gen: gen-build gen-client-ts

#########################
## Web
Expand Down
14 changes: 10 additions & 4 deletions authentik/admin/api/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from rest_framework.views import APIView

from authentik.core.api.utils import PassiveSerializer
from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST
from authentik.outposts.models import Outpost
Expand All @@ -37,8 +38,9 @@ class SystemInfoSerializer(PassiveSerializer):
http_host = SerializerMethodField()
http_is_secure = SerializerMethodField()
runtime = SerializerMethodField()
tenant = SerializerMethodField()
brand = SerializerMethodField()
server_time = SerializerMethodField()
embedded_outpost_disabled = SerializerMethodField()
embedded_outpost_host = SerializerMethodField()

def get_http_headers(self, request: Request) -> dict[str, str]:
Expand Down Expand Up @@ -69,14 +71,18 @@ def get_runtime(self, request: Request) -> RuntimeDict:
"uname": " ".join(platform.uname()),
}

def get_tenant(self, request: Request) -> str:
"""Currently active tenant"""
return str(request._request.tenant)
def get_brand(self, request: Request) -> str:
"""Currently active brand"""
return str(request._request.brand)

def get_server_time(self, request: Request) -> datetime:
"""Current server time"""
return now()

def get_embedded_outpost_disabled(self, request: Request) -> bool:
"""Whether the embedded outpost is disabled"""
return CONFIG.get_bool("outposts.disable_embedded_outpost", False)

def get_embedded_outpost_host(self, request: Request) -> str:
"""Get the FQDN configured on the embedded outpost"""
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
Expand Down
2 changes: 1 addition & 1 deletion authentik/admin/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ class AuthentikAdminConfig(ManagedAppConfig):
verbose_name = "authentik Admin"
default = True

def reconcile_load_admin_signals(self):
def reconcile_global_load_admin_signals(self):
"""Load admin signals"""
self.import_module("authentik.admin.signals")
2 changes: 1 addition & 1 deletion authentik/api/templates/api/browser.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{% load static %}

{% block title %}
API Browser - {{ tenant.branding_title }}
API Browser - {{ brand.branding_title }}
{% endblock %}

{% block head %}
Expand Down
2 changes: 1 addition & 1 deletion authentik/api/v3/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def get_capabilities(self) -> list[Capabilities]:
for processor in get_context_processors():
if cap := processor.capability():
caps.append(cap)
if CONFIG.get_bool("impersonation"):
if self.request.tenant.impersonation:
caps.append(Capabilities.CAN_IMPERSONATE)
if settings.DEBUG: # pragma: no cover
caps.append(Capabilities.CAN_DEBUG)
Expand Down
37 changes: 31 additions & 6 deletions authentik/blueprints/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ class ManagedAppConfig(AppConfig):

_logger: BoundLogger

RECONCILE_GLOBAL_PREFIX: str = "reconcile_global_"
RECONCILE_TENANT_PREFIX: str = "reconcile_tenant_"

def __init__(self, app_name: str, *args, **kwargs) -> None:
super().__init__(app_name, *args, **kwargs)
self._logger = get_logger().bind(app_name=app_name)

def ready(self) -> None:
self.reconcile()
self.reconcile_global()
self.reconcile_tenant()
return super().ready()

def import_module(self, path: str):
"""Load module"""
import_module(path)

def reconcile(self) -> None:
"""reconcile ourselves"""
prefix = "reconcile_"
def _reconcile(self, prefix: str) -> None:
for meth_name in dir(self):
meth = getattr(self, meth_name)
if not ismethod(meth):
Expand All @@ -42,6 +44,29 @@ def reconcile(self) -> None:
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.warning("Failed to run reconcile", name=name, exc=exc)

def reconcile_tenant(self) -> None:
"""reconcile ourselves for tenanted methods"""
from authentik.tenants.models import Tenant

try:
tenants = list(Tenant.objects.filter(ready=True))
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.debug("Failed to get tenants to run reconcile", exc=exc)
return
for tenant in tenants:
with tenant:
self._reconcile(self.RECONCILE_TENANT_PREFIX)

def reconcile_global(self) -> None:
"""
reconcile ourselves for global methods.
Used for signals, tasks, etc. Database queries should not be made in here.
"""
from django_tenants.utils import get_public_schema_name, schema_context

with schema_context(get_public_schema_name()):
self._reconcile(self.RECONCILE_GLOBAL_PREFIX)


class AuthentikBlueprintsConfig(ManagedAppConfig):
"""authentik Blueprints app"""
Expand All @@ -51,11 +76,11 @@ class AuthentikBlueprintsConfig(ManagedAppConfig):
verbose_name = "authentik Blueprints"
default = True

def reconcile_load_blueprints_v1_tasks(self):
def reconcile_global_load_blueprints_v1_tasks(self):
"""Load v1 tasks"""
self.import_module("authentik.blueprints.v1.tasks")

def reconcile_blueprints_discovery(self):
def reconcile_tenant_blueprints_discovery(self):
"""Run blueprint discovery"""
from authentik.blueprints.v1.tasks import blueprints_discovery, clear_failed_blueprints

Expand Down
19 changes: 11 additions & 8 deletions authentik/blueprints/management/commands/apply_blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.v1.importer import Importer
from authentik.tenants.models import Tenant

LOGGER = get_logger()

Expand All @@ -16,14 +17,16 @@ class Command(BaseCommand):
@no_translations
def handle(self, *args, **options):
"""Apply all blueprints in order, abort when one fails to import"""
for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer.from_string(content)
valid, _ = importer.validate()
if not valid:
self.stderr.write("blueprint invalid")
sys_exit(1)
importer.apply()
for tenant in Tenant.objects.filter(ready=True):
with tenant:
for blueprint_path in options.get("blueprints", []):
content = BlueprintInstance(path=blueprint_path).retrieve()
importer = Importer.from_string(content)
valid, _ = importer.validate()
if not valid:
self.stderr.write("blueprint invalid")
sys_exit(1)
importer.apply()

def add_arguments(self, parser):
parser.add_argument("blueprints", nargs="+", type=str)
7 changes: 4 additions & 3 deletions authentik/blueprints/management/commands/export_blueprint.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
"""Export blueprint of current authentik install"""
from django.core.management.base import BaseCommand, no_translations
from django.core.management.base import no_translations
from structlog.stdlib import get_logger

from authentik.blueprints.v1.exporter import Exporter
from authentik.tenants.management import TenantCommand

LOGGER = get_logger()


class Command(BaseCommand):
class Command(TenantCommand):
"""Export blueprint of current authentik install"""

@no_translations
def handle(self, *args, **options):
def handle_per_tenant(self, *args, **options):
"""Export blueprint of current authentik install"""
exporter = Exporter()
self.stdout.write(exporter.export_to_string())
11 changes: 6 additions & 5 deletions authentik/blueprints/migrations/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from authentik.lib.config import CONFIG


def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
def check_blueprint_v1_file(BlueprintInstance: type, db_alias, path: Path):
"""Check if blueprint should be imported"""
from authentik.blueprints.models import BlueprintInstanceStatus
from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata
Expand All @@ -29,15 +29,17 @@ def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
if version != 1:
return
blueprint_file.seek(0)
instance: BlueprintInstance = BlueprintInstance.objects.filter(path=path).first()
instance: BlueprintInstance = (
BlueprintInstance.objects.using(db_alias).filter(path=path).first()
)
rel_path = path.relative_to(Path(CONFIG.get("blueprints_dir")))
meta = None
if metadata:
meta = from_dict(BlueprintMetadata, metadata)
if meta.labels.get(LABEL_AUTHENTIK_INSTANTIATE, "").lower() == "false":
return
if not instance:
instance = BlueprintInstance(
BlueprintInstance.objects.using(db_alias).create(
name=meta.name if meta else str(rel_path),
path=str(rel_path),
context={},
Expand All @@ -47,7 +49,6 @@ def check_blueprint_v1_file(BlueprintInstance: type, path: Path):
last_applied_hash="",
metadata=metadata or {},
)
instance.save()


def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
Expand All @@ -56,7 +57,7 @@ def migration_blueprint_import(apps: Apps, schema_editor: BaseDatabaseSchemaEdit

db_alias = schema_editor.connection.alias
for file in glob(f"{CONFIG.get('blueprints_dir')}/**/*.yaml", recursive=True):
check_blueprint_v1_file(BlueprintInstance, Path(file))
check_blueprint_v1_file(BlueprintInstance, db_alias, Path(file))

for blueprint in BlueprintInstance.objects.using(db_alias).all():
# If we already have flows (and we should always run before flow migrations)
Expand Down
2 changes: 1 addition & 1 deletion authentik/blueprints/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def wrapper_outer(func: Callable):
def wrapper(*args, **kwargs):
config = apps.get_app_config(app_name)
if isinstance(config, ManagedAppConfig):
config.reconcile()
config.ready()
return func(*args, **kwargs)

return wrapper
Expand Down
6 changes: 3 additions & 3 deletions authentik/blueprints/tests/test_packaged.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@
from authentik.blueprints.models import BlueprintInstance
from authentik.blueprints.tests import apply_blueprint
from authentik.blueprints.v1.importer import Importer
from authentik.tenants.models import Tenant
from authentik.brands.models import Brand


class TestPackaged(TransactionTestCase):
"""Empty class, test methods are added dynamically"""

@apply_blueprint("default/default-tenant.yaml")
@apply_blueprint("default/default-brand.yaml")
def test_decorator_static(self):
"""Test @apply_blueprint decorator"""
self.assertTrue(Tenant.objects.filter(domain="authentik-default").exists())
self.assertTrue(Brand.objects.filter(domain="authentik-default").exists())


def blueprint_tester(file_name: Path) -> Callable:
Expand Down
2 changes: 2 additions & 0 deletions authentik/blueprints/v1/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from authentik.outposts.models import OutpostServiceConnection
from authentik.policies.models import Policy, PolicyBindingModel
from authentik.providers.scim.models import SCIMGroup, SCIMUser
from authentik.tenants.models import Tenant

# Context set when the serializer is created in a blueprint context
# Update website/developer-docs/blueprints/v1/models.md when used
Expand All @@ -57,6 +58,7 @@ def excluded_models() -> list[type[Model]]:
from django.contrib.auth.models import User as DjangoUser

return (
Tenant,
DjangoUser,
DjangoGroup,
# Base classes
Expand Down
20 changes: 13 additions & 7 deletions authentik/blueprints/v1/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from authentik.events.utils import sanitize_dict
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
from authentik.tenants.models import Tenant

LOGGER = get_logger()
_file_watcher_started = False
Expand Down Expand Up @@ -78,13 +79,18 @@ def on_any_event(self, event: FileSystemEvent):
root = Path(CONFIG.get("blueprints_dir")).absolute()
path = Path(event.src_path).absolute()
rel_path = str(path.relative_to(root))
if isinstance(event, FileCreatedEvent):
LOGGER.debug("new blueprint file created, starting discovery", path=rel_path)
blueprints_discovery.delay(rel_path)
if isinstance(event, FileModifiedEvent):
for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
LOGGER.debug("modified blueprint file, starting apply", instance=instance)
apply_blueprint.delay(instance.pk.hex)
for tenant in Tenant.objects.filter(ready=True):
with tenant:
root = Path(CONFIG.get("blueprints_dir")).absolute()
path = Path(event.src_path).absolute()
rel_path = str(path.relative_to(root))
if isinstance(event, FileCreatedEvent):
LOGGER.debug("new blueprint file created, starting discovery", path=rel_path)
blueprints_discovery.delay(rel_path)
if isinstance(event, FileModifiedEvent):
for instance in BlueprintInstance.objects.filter(path=rel_path, enabled=True):
LOGGER.debug("modified blueprint file, starting apply", instance=instance)
apply_blueprint.delay(instance.pk.hex)


@CELERY_APP.task(
Expand Down
Empty file added authentik/brands/__init__.py
Empty file.
Loading

0 comments on commit 780d1a2

Please sign in to comment.