Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enterprise/providers: Add RAC [AUTH-15] #7291

Merged
merged 72 commits into from
Dec 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
a513f9e
add basic guacamole
BeryJu Jan 12, 2023
736aabe
make everything mostly work
BeryJu Oct 15, 2023
51e86c7
add rac build to CI
BeryJu Oct 15, 2023
17497cc
fix resize, fix web lint, sendSize correctly
BeryJu Oct 15, 2023
5add409
pre-send connection from client, format
BeryJu Oct 15, 2023
e83451f
improve throughput
BeryJu Oct 15, 2023
6600f6d
cleanup
BeryJu Oct 15, 2023
735978b
rework TokenOutpostConsumer into middleware
BeryJu Oct 15, 2023
f063f65
fix some layout issues
BeryJu Oct 15, 2023
ed5bd9c
add outpost controllers
BeryJu Oct 15, 2023
546053c
start testing audio things
BeryJu Oct 15, 2023
2c689c5
fix a bunch of things
BeryJu Oct 16, 2023
6b634b0
add deps
BeryJu Oct 16, 2023
00ae0f1
fix to work with outpost group
BeryJu Oct 16, 2023
5f77b51
add simple loadbalancing
BeryJu Oct 16, 2023
62cd111
add simple reconnect
BeryJu Oct 16, 2023
6641cdd
show reconnecting text
BeryJu Oct 17, 2023
066ff89
fix error when checking ports
BeryJu Oct 17, 2023
9b9fa49
move to providers
BeryJu Oct 25, 2023
0fe11c0
add flow check to interface
BeryJu Oct 25, 2023
a7004e2
fix go lint
BeryJu Oct 25, 2023
3de2359
fix rac app label
BeryJu Nov 6, 2023
a25663c
fix audio
BeryJu Nov 6, 2023
df69836
add logging
BeryJu Nov 6, 2023
9fa8483
cleanup
BeryJu Nov 8, 2023
6f6c864
allow overriding all settings
BeryJu Nov 9, 2023
57136cb
fix duplicate keyboard, debug high DPI
BeryJu Nov 9, 2023
c905019
re-add deps
BeryJu Nov 14, 2023
f26a7c9
fix lint
BeryJu Nov 14, 2023
018d898
fix missing __init__.py breaking model loading
BeryJu Nov 14, 2023
b37d91b
fix tests
BeryJu Nov 14, 2023
17bc1d3
bump successful ws connection to info
BeryJu Nov 23, 2023
668993f
hide cursor since guac draws that
BeryJu Nov 23, 2023
7f3397d
add clipboard support (bidirectional)
BeryJu Nov 23, 2023
01a962e
make codespell not want to break the code
BeryJu Nov 23, 2023
d132a58
run pr comment in separate task
BeryJu Nov 24, 2023
51bac2c
start endpoint and property mapping stuff
BeryJu Dec 2, 2023
7b83408
more endpoint things
BeryJu Dec 23, 2023
9742848
unrelated: fix event model_pk filtering with ints
BeryJu Dec 23, 2023
e23c11d
unrelated: improve event display for changelog
BeryJu Dec 26, 2023
7e75040
rebuild endpoint stuff again
BeryJu Dec 26, 2023
f51a3a0
idk special url
BeryJu Dec 26, 2023
065d2c9
more stuff, connect token with session
BeryJu Dec 26, 2023
1d1af33
add disconnect
BeryJu Dec 26, 2023
97b4ba4
rework disconnect
BeryJu Dec 27, 2023
2be9d5a
clear cache when creating outpost
BeryJu Dec 27, 2023
67324e1
support host:port and fix protocol
BeryJu Dec 27, 2023
6259836
center smaller viewport
BeryJu Dec 27, 2023
16d5271
rework connection to wait more and stop after some time
BeryJu Dec 27, 2023
50c6e1c
add policy control to endpoints
BeryJu Dec 27, 2023
f1398ce
remove provider protocol
BeryJu Dec 27, 2023
4658e0c
don't switch to different outpost connection when already chosen
BeryJu Dec 27, 2023
2299575
start using property mappings, add static settings
BeryJu Dec 27, 2023
432a05a
add some RAC mapping settings
BeryJu Dec 27, 2023
a4ef0b9
fix lint
BeryJu Dec 27, 2023
81ccc3a
start adding tests
BeryJu Dec 28, 2023
b26e54c
add tests for event changes
BeryJu Dec 28, 2023
de38a95
add tests and fix issues found by said tests
BeryJu Dec 28, 2023
fcdf2aa
add preview banner, move endpoints to main page
BeryJu Dec 28, 2023
bfc9311
add locale
BeryJu Dec 28, 2023
952dafd
auto-select endpoint if only one is available
BeryJu Dec 28, 2023
ae2e8ca
backport https://github.com/goauthentik/authentik/pull/7831 to rac
BeryJu Dec 29, 2023
8188ca2
dont select property mappings on endpoints
BeryJu Dec 29, 2023
d556825
make table modal only load when opened
BeryJu Dec 29, 2023
93ef138
only auto-redirect when open
BeryJu Dec 29, 2023
c2bd19b
fix web deps
BeryJu Dec 29, 2023
ddc9687
check for token expiry and terminate session
BeryJu Dec 29, 2023
5309f02
re-add endpoint name to title
BeryJu Dec 29, 2023
4546bac
disconnect connection when token is manually deleted
BeryJu Dec 29, 2023
4bef6bd
add initial RAC docs
BeryJu Dec 29, 2023
97d6fc0
add connection expiry setting to provider
BeryJu Dec 29, 2023
7561c9b
fix flaky tests
BeryJu Dec 29, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 @@
def check_license(self):
"""Check license"""
if not LicenseKey.get_total().is_valid():
return False
return PolicyResult(False, _("Enterprise required to access this feature."))

Check warning on line 18 in authentik/enterprise/policy.py

View check run for this annotation

Codecov / codecov/patch

authentik/enterprise/policy.py#L18

Added line #L18 was not covered by tests
if self.request.user.type != UserTypes.INTERNAL:
return False
return True
return PolicyResult(False, _("Feature only accessible for internal users."))

Check warning on line 20 in authentik/enterprise/policy.py

View check run for this annotation

Codecov / codecov/patch

authentik/enterprise/policy.py#L20

Added line #L20 was not covered by tests
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

Check warning on line 47 in authentik/enterprise/providers/rac/api/endpoints.py

View check run for this annotation

Codecov / codecov/patch

authentik/enterprise/providers/rac/api/endpoints.py#L46-L47

Added lines #L46 - L47 were not covered by tests

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)

Check warning on line 121 in authentik/enterprise/providers/rac/api/endpoints.py

View check run for this annotation

Codecov / codecov/patch

authentik/enterprise/providers/rac/api/endpoints.py#L121

Added line #L121 was not covered by tests
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)

Check warning on line 21 in authentik/enterprise/providers/rac/api/property_mappings.py

View check run for this annotation

Codecov / codecov/patch

authentik/enterprise/providers/rac/api/property_mappings.py#L19-L21

Added lines #L19 - L21 were not covered by tests

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
Loading