Skip to content

Commit

Permalink
Merge branch 'main' into web/issue-4880-multi-select-limitations
Browse files Browse the repository at this point in the history
* main: (30 commits)
  outposts/proxy: better Redis error message (#8044)
  translate: Updates for file web/xliff/en.xlf in fr (#8046)
  web: bump the eslint group in /tests/wdio with 2 updates (#8041)
  web: bump the storybook group in /web with 7 updates (#8042)
  web: bump the eslint group in /web with 2 updates (#8043)
  web: bump @types/guacamole-common-js from 1.3.2 to 1.5.2 in /web (#8030)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#8038)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#8039)
  website: bump clsx from 2.0.0 to 2.1.0 in /website (#8033)
  core: bump golang from 1.21.3-bookworm to 1.21.5-bookworm (#8027)
  web: bump the babel group in /web with 4 updates (#8028)
  web: bump the esbuild group in /web with 2 updates (#8029)
  web: bump rollup from 4.9.1 to 4.9.2 in /web (#8031)
  tests/e2e: fix tests to work without docker network_mode host (#8035)
  website/docs: fix typo (#8015)
  web: bump API Client version (#8025)
  enterprise/providers: Add RAC [AUTH-15] (#7291)
  outposts: disable deployment and secret reconciler for embedded outpost in code instead of in config (#8021)
  providers/proxy: use access token (#8022)
  website/integrations: Add custom Group/Role mapping documentation for Grafana (#7453)
  ...
  • Loading branch information
kensternberg-authentik committed Jan 2, 2024
2 parents 7c4fafd + d54b410 commit 73bba04
Show file tree
Hide file tree
Showing 146 changed files with 8,504 additions and 1,690 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ blueprints/local
.git
!gen-ts-api/node_modules
!gen-ts-api/dist/**
!gen-go-api/
1 change: 1 addition & 0 deletions .github/codespell-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ keypair
keypairs
hass
warmup
ontext
29 changes: 23 additions & 6 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,12 +249,6 @@ jobs:
VERSION_FAMILY=${{ steps.ev.outputs.versionFamily }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Comment on PR
if: github.event_name == 'pull_request'
continue-on-error: true
uses: ./.github/actions/comment-pr-instructions
with:
tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}
build-arm64:
needs: ci-core-mark
runs-on: ubuntu-latest
Expand Down Expand Up @@ -303,3 +297,26 @@ jobs:
platforms: linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
pr-comment:
needs:
- build
- build-arm64
runs-on: ubuntu-latest
if: ${{ github.event_name == 'pull_request' }}
permissions:
# Needed to write comments on PRs
pull-requests: write
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: prepare variables
uses: ./.github/actions/docker-push-variables
id: ev
env:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
- name: Comment on PR
uses: ./.github/actions/comment-pr-instructions
with:
tag: gh-${{ steps.ev.outputs.branchNameContainer }}-${{ steps.ev.outputs.timestamp }}-${{ steps.ev.outputs.shortHash }}
2 changes: 2 additions & 0 deletions .github/workflows/ci-outpost.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
- proxy
- ldap
- radius
- rac
runs-on: ubuntu-latest
permissions:
# Needed to upload contianer images to ghcr.io
Expand Down Expand Up @@ -119,6 +120,7 @@ jobs:
- proxy
- ldap
- radius
- rac
goos: [linux]
goarch: [amd64, arm64]
steps:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
- proxy
- ldap
- radius
- rac
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test: ## Run the server tests and produce a coverage report (locally)
lint-fix: ## Lint and automatically fix errors in the python source code. Reports spelling errors.
isort $(PY_SOURCES)
black $(PY_SOURCES)
ruff $(PY_SOURCES)
ruff --fix $(PY_SOURCES)
codespell -w $(CODESPELL_ARGS)

lint: ## Lint the python and golang sources
Expand Down
2 changes: 1 addition & 1 deletion authentik/blueprints/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def reconcile(self) -> None:
meth()
self._logger.debug("Successfully reconciled", name=name)
except (DatabaseError, ProgrammingError, InternalError) as exc:
self._logger.debug("Failed to run reconcile", name=name, exc=exc)
self._logger.warning("Failed to run reconcile", name=name, exc=exc)


class AuthentikBlueprintsConfig(ManagedAppConfig):
Expand Down
21 changes: 14 additions & 7 deletions authentik/core/channels.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,29 @@
"""Channels base classes"""
from channels.db import database_sync_to_async
from channels.exceptions import DenyConnection
from channels.generic.websocket import JsonWebsocketConsumer
from rest_framework.exceptions import AuthenticationFailed
from structlog.stdlib import get_logger

from authentik.api.authentication import bearer_auth
from authentik.core.models import User

LOGGER = get_logger()


class AuthJsonConsumer(JsonWebsocketConsumer):
class TokenOutpostMiddleware:
"""Authorize a client with a token"""

user: User
def __init__(self, inner):
self.inner = inner

def connect(self):
headers = dict(self.scope["headers"])
async def __call__(self, scope, receive, send):
scope = dict(scope)
await self.auth(scope)
return await self.inner(scope, receive, send)

@database_sync_to_async
def auth(self, scope):
"""Authenticate request from header"""
headers = dict(scope["headers"])
if b"authorization" not in headers:
LOGGER.warning("WS Request without authorization header")
raise DenyConnection()
Expand All @@ -32,4 +39,4 @@ def connect(self):
LOGGER.warning("Failed to authenticate", exc=exc)
raise DenyConnection()

self.user = user
scope["user"] = user
1 change: 1 addition & 0 deletions authentik/core/views/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["version_family"] = f"{LOCAL_VERSION.major}.{LOCAL_VERSION.minor}"
kwargs["version_subdomain"] = f"version-{LOCAL_VERSION.major}-{LOCAL_VERSION.minor}"
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
return super().get_context_data(**kwargs)


Expand Down
10 changes: 6 additions & 4 deletions authentik/enterprise/policy.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Enterprise license policies"""
from typing import Optional

from django.utils.translation import gettext_lazy as _

from authentik.core.models import User, UserTypes
from authentik.enterprise.models import LicenseKey
from authentik.policies.types import PolicyRequest, PolicyResult
Expand All @@ -13,18 +15,18 @@ class EnterprisePolicyAccessView(PolicyAccessView):
def check_license(self):
"""Check license"""
if not LicenseKey.get_total().is_valid():
return False
return PolicyResult(False, _("Enterprise required to access this feature."))
if self.request.user.type != UserTypes.INTERNAL:
return False
return True
return PolicyResult(False, _("Feature only accessible for internal users."))
return PolicyResult(True)

def user_has_access(self, user: Optional[User] = None) -> PolicyResult:
user = user or self.request.user
request = PolicyRequest(user)
request.http_request = self.request
result = super().user_has_access(user)
enterprise_result = self.check_license()
if not enterprise_result:
if not enterprise_result.passing:
return enterprise_result
return result

Expand Down
Empty file.
Empty file.
Empty file.
133 changes: 133 additions & 0 deletions authentik/enterprise/providers/rac/api/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""RAC Provider API Views"""
from typing import Optional

from django.core.cache import cache
from django.db.models import QuerySet
from django.urls import reverse
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger

from authentik.core.api.used_by import UsedByMixin
from authentik.core.models import Provider
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer
from authentik.enterprise.providers.rac.models import Endpoint
from authentik.policies.engine import PolicyEngine
from authentik.rbac.filters import ObjectFilter

LOGGER = get_logger()


def user_endpoint_cache_key(user_pk: str) -> str:
"""Cache key where endpoint list for user is saved"""
return f"goauthentik.io/providers/rac/endpoint_access/{user_pk}"


class EndpointSerializer(ModelSerializer):
"""Endpoint Serializer"""

provider_obj = RACProviderSerializer(source="provider", read_only=True)
launch_url = SerializerMethodField()

def get_launch_url(self, endpoint: Endpoint) -> Optional[str]:
"""Build actual launch URL (the provider itself does not have one, just
individual endpoints)"""
try:
# pylint: disable=no-member
return reverse(
"authentik_providers_rac:start",
kwargs={"app": endpoint.provider.application.slug, "endpoint": endpoint.pk},
)
except Provider.application.RelatedObjectDoesNotExist:
return None

class Meta:
model = Endpoint
fields = [
"pk",
"name",
"provider",
"provider_obj",
"protocol",
"host",
"settings",
"property_mappings",
"auth_mode",
"launch_url",
]


class EndpointViewSet(UsedByMixin, ModelViewSet):
"""Endpoint Viewset"""

queryset = Endpoint.objects.all()
serializer_class = EndpointSerializer
filterset_fields = ["name", "provider"]
search_fields = ["name", "protocol"]
ordering = ["name", "protocol"]

def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
"""Custom filter_queryset method which ignores guardian, but still supports sorting"""
for backend in list(self.filter_backends):
if backend == ObjectFilter:
continue
queryset = backend().filter_queryset(self.request, queryset, self)
return queryset

def _get_allowed_endpoints(self, queryset: QuerySet) -> list[Endpoint]:
endpoints = []
for endpoint in queryset:
engine = PolicyEngine(endpoint, self.request.user, self.request)
engine.build()
if engine.passing:
endpoints.append(endpoint)
return endpoints

@extend_schema(
parameters=[
OpenApiParameter(
"search",
OpenApiTypes.STR,
),
OpenApiParameter(
name="superuser_full_list",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
),
],
responses={
200: EndpointSerializer(many=True),
400: OpenApiResponse(description="Bad request"),
},
)
def list(self, request: Request, *args, **kwargs) -> Response:
"""List accessible endpoints"""
should_cache = request.GET.get("search", "") == ""

superuser_full_list = str(request.GET.get("superuser_full_list", "false")).lower() == "true"
if superuser_full_list and request.user.is_superuser:
return super().list(request)

queryset = self._filter_queryset_for_list(self.get_queryset())
self.paginate_queryset(queryset)

allowed_endpoints = []
if not should_cache:
allowed_endpoints = self._get_allowed_endpoints(queryset)
if should_cache:
allowed_endpoints = cache.get(user_endpoint_cache_key(self.request.user.pk))
if not allowed_endpoints:
LOGGER.debug("Caching allowed endpoint list")
allowed_endpoints = self._get_allowed_endpoints(queryset)
cache.set(
user_endpoint_cache_key(self.request.user.pk),
allowed_endpoints,
timeout=86400,
)
serializer = self.get_serializer(allowed_endpoints, many=True)
return self.get_paginated_response(serializer.data)
35 changes: 35 additions & 0 deletions authentik/enterprise/providers/rac/api/property_mappings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""RAC Provider API Views"""
from rest_framework.fields import CharField
from rest_framework.viewsets import ModelViewSet

from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField
from authentik.enterprise.providers.rac.models import RACPropertyMapping


class RACPropertyMappingSerializer(PropertyMappingSerializer):
"""RACPropertyMapping Serializer"""

static_settings = JSONDictField()
expression = CharField(allow_blank=True, required=False)

def validate_expression(self, expression: str) -> str:
"""Test Syntax"""
if expression == "":
return expression
return super().validate_expression(expression)

class Meta:
model = RACPropertyMapping
fields = PropertyMappingSerializer.Meta.fields + ["static_settings"]


class RACPropertyMappingViewSet(UsedByMixin, ModelViewSet):
"""RACPropertyMapping Viewset"""

queryset = RACPropertyMapping.objects.all()
serializer_class = RACPropertyMappingSerializer
search_fields = ["name"]
ordering = ["name"]
filterset_fields = ["name", "managed"]
31 changes: 31 additions & 0 deletions authentik/enterprise/providers/rac/api/providers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""RAC Provider API Views"""
from rest_framework.fields import CharField, ListField
from rest_framework.viewsets import ModelViewSet

from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.rac.models import RACProvider


class RACProviderSerializer(ProviderSerializer):
"""RACProvider Serializer"""

outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")

class Meta:
model = RACProvider
fields = ProviderSerializer.Meta.fields + ["settings", "outpost_set", "connection_expiry"]
extra_kwargs = ProviderSerializer.Meta.extra_kwargs


class RACProviderViewSet(UsedByMixin, ModelViewSet):
"""RACProvider Viewset"""

queryset = RACProvider.objects.all()
serializer_class = RACProviderSerializer
filterset_fields = {
"application": ["isnull"],
"name": ["iexact"],
}
search_fields = ["name"]
ordering = ["name"]
17 changes: 17 additions & 0 deletions authentik/enterprise/providers/rac/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""RAC app config"""
from authentik.blueprints.apps import ManagedAppConfig


class AuthentikEnterpriseProviderRAC(ManagedAppConfig):
"""authentik enterprise rac app config"""

name = "authentik.enterprise.providers.rac"
label = "authentik_providers_rac"
verbose_name = "authentik Enterprise.Providers.RAC"
default = True
mountpoint = ""
ws_mountpoint = "authentik.enterprise.providers.rac.urls"

def reconcile_load_rac_signals(self):
"""Load rac signals"""
self.import_module("authentik.enterprise.providers.rac.signals")
Loading

0 comments on commit 73bba04

Please sign in to comment.