Compare commits
97 Commits
imports-fo
...
eap-but-ac
Author | SHA1 | Date | |
---|---|---|---|
06848be14b | |||
4bae3bbe60 | |||
e33f839d7f | |||
f5eb827d14 | |||
9045f5ba73 | |||
7b97e92094 | |||
3027cdcc4b | |||
67f627a925 | |||
f1101e0c01 | |||
fb01a117ad | |||
fad18db70b | |||
e0c837257c | |||
2a567ccc85 | |||
e36373ceab | |||
d8a625be03 | |||
4d944f7444 | |||
c49274042b | |||
10fc15ffe0 | |||
7c996d9d9d | |||
5d25f68b71 | |||
8da54d5811 | |||
4571f5e644 | |||
ee234ea3aa | |||
82c177b7eb | |||
1155ccb3e8 | |||
1575b96262 | |||
19bb77638a | |||
d6cf129eaa | |||
b6686cff14 | |||
8cf8f1e199 | |||
50c50c4109 | |||
51f4a8d83d | |||
3ada3a7e0e | |||
fa06c9fe4e | |||
2a024238fe | |||
91c87b7c3c | |||
318443f270 | |||
ac88784089 | |||
855afa7b9f | |||
240abfef41 | |||
03075f1890 | |||
5bc0ed6e11 | |||
8f4cfc28c7 | |||
6d77eaaab7 | |||
9cee59537c | |||
fc5c0e2789 | |||
573446689f | |||
fd4bfe604d | |||
06e76a5b37 | |||
3c228bf5c3 | |||
8a80f07db2 | |||
ae59a3e576 | |||
df21e678d6 | |||
a71532b3e3 | |||
d7cb0b3ea1 | |||
ba8f137885 | |||
958ff66070 | |||
ad57c66a32 | |||
2bba0ddd74 | |||
767c0a8e45 | |||
b10c795a26 | |||
8088e08fd9 | |||
eab6e288d7 | |||
91c2863358 | |||
1638e95bc7 | |||
8f75131541 | |||
c85471575a | |||
5d00dc7e9e | |||
6982e7d1c9 | |||
c7fe987c5a | |||
e48739c8a0 | |||
b2ee585c43 | |||
97e8ea8e76 | |||
1f1e0c9db1 | |||
ca47a803fe | |||
c606eb53b0 | |||
62357133b0 | |||
99d2d91257 | |||
69d9363fce | |||
cfc7f6b993 | |||
bebbbe9b90 | |||
188d3c69c1 | |||
877f312145 | |||
f471a98bc7 | |||
e874cfc21d | |||
ec7bdf74aa | |||
e87bc94b95 | |||
a3865abaa9 | |||
7100d3c674 | |||
c0c2d2ad3c | |||
dc287989db | |||
03204f6943 | |||
fcd369e466 | |||
cb79407bc1 | |||
04a88daf34 | |||
c6a49da5c3 | |||
bfeeecf3fa |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2025.6.2
|
||||
current_version = 2025.6.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
|
||||
|
@ -38,6 +38,8 @@ jobs:
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: docker/setup-qemu-action@v3.6.0
|
||||
|
1
.github/workflows/ci-main-daily.yml
vendored
1
.github/workflows/ci-main-daily.yml
vendored
@ -9,6 +9,7 @@ on:
|
||||
|
||||
jobs:
|
||||
test-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
4
.github/workflows/ci-main.yml
vendored
4
.github/workflows/ci-main.yml
vendored
@ -247,11 +247,13 @@ jobs:
|
||||
# Needed for attestation
|
||||
id-token: write
|
||||
attestations: write
|
||||
# Needed for checkout
|
||||
contents: read
|
||||
needs: ci-core-mark
|
||||
uses: ./.github/workflows/_reusable-docker-build.yaml
|
||||
secrets: inherit
|
||||
with:
|
||||
image_name: ghcr.io/goauthentik/dev-server
|
||||
image_name: ${{ github.repository == 'goauthentik/authentik-internal' && 'ghcr.io/goauthentik/internal-server' || 'ghcr.io/goauthentik/dev-server' }}
|
||||
release: false
|
||||
pr-comment:
|
||||
needs:
|
||||
|
1
.github/workflows/ci-outpost.yml
vendored
1
.github/workflows/ci-outpost.yml
vendored
@ -59,6 +59,7 @@ jobs:
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
build-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
timeout-minutes: 120
|
||||
needs:
|
||||
- ci-outpost-mark
|
||||
|
2
.github/workflows/ci-website.yml
vendored
2
.github/workflows/ci-website.yml
vendored
@ -63,6 +63,7 @@ jobs:
|
||||
working-directory: website/
|
||||
run: npm run ${{ matrix.job }}
|
||||
build-container:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# Needed to upload container images to ghcr.io
|
||||
@ -122,3 +123,4 @@ jobs:
|
||||
- uses: re-actors/alls-green@release/v1
|
||||
with:
|
||||
jobs: ${{ toJSON(needs) }}
|
||||
allowed-skips: ${{ github.repository == 'goauthentik/authentik-internal' && 'build-container' || '[]' }}
|
||||
|
21
.github/workflows/repo-mirror-cleanup.yml
vendored
Normal file
21
.github/workflows/repo-mirror-cleanup.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
name: "authentik-repo-mirror-cleanup"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
to_internal:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
|
||||
with:
|
||||
target_repo_url: git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
|
||||
args: --tags --force --prune
|
||||
env:
|
||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
9
.github/workflows/repo-mirror.yml
vendored
9
.github/workflows/repo-mirror.yml
vendored
@ -11,11 +11,10 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- if: ${{ env.MIRROR_KEY != '' }}
|
||||
uses: pixta-dev/repository-mirroring-action@v1
|
||||
uses: BeryJu/repository-mirroring-action@5cf300935bc2e068f73ea69bcc411a8a997208eb
|
||||
with:
|
||||
target_repo_url:
|
||||
git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key:
|
||||
${{ secrets.GH_MIRROR_KEY }}
|
||||
target_repo_url: git@github.com:goauthentik/authentik-internal.git
|
||||
ssh_private_key: ${{ secrets.GH_MIRROR_KEY }}
|
||||
args: --tags --force
|
||||
env:
|
||||
MIRROR_KEY: ${{ secrets.GH_MIRROR_KEY }}
|
||||
|
@ -16,6 +16,7 @@ env:
|
||||
|
||||
jobs:
|
||||
compile:
|
||||
if: ${{ github.repository != 'goauthentik/authentik-internal' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: generate_token
|
||||
|
@ -75,7 +75,7 @@ RUN --mount=type=secret,id=GEOIPUPDATE_ACCOUNT_ID \
|
||||
/bin/sh -c "GEOIPUPDATE_LICENSE_KEY_FILE=/run/secrets/GEOIPUPDATE_LICENSE_KEY /usr/bin/entry.sh || echo 'Failed to get GeoIP database, disabling'; exit 0"
|
||||
|
||||
# Stage 4: Download uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.15 AS uv
|
||||
FROM ghcr.io/astral-sh/uv:0.7.17 AS uv
|
||||
# Stage 5: Base python image
|
||||
FROM ghcr.io/goauthentik/fips-python:3.13.5-slim-bookworm-fips AS python-base
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from os import environ
|
||||
|
||||
__version__ = "2025.6.2"
|
||||
__version__ = "2025.6.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -5,7 +5,6 @@ from collections.abc import Callable
|
||||
from django.apps import apps
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.blueprints.v1.importer import is_model_allowed
|
||||
from authentik.lib.models import SerializerModel
|
||||
from authentik.providers.oauth2.models import RefreshToken
|
||||
|
||||
@ -22,10 +21,13 @@ def serializer_tester_factory(test_model: type[SerializerModel]) -> Callable:
|
||||
return
|
||||
model_class = test_model()
|
||||
self.assertTrue(isinstance(model_class, SerializerModel))
|
||||
# Models that have subclasses don't have to have a serializer
|
||||
if len(test_model.__subclasses__()) > 0:
|
||||
return
|
||||
self.assertIsNotNone(model_class.serializer)
|
||||
if model_class.serializer.Meta().model == RefreshToken:
|
||||
return
|
||||
self.assertEqual(model_class.serializer.Meta().model, test_model)
|
||||
self.assertTrue(issubclass(test_model, model_class.serializer.Meta().model))
|
||||
|
||||
return tester
|
||||
|
||||
@ -34,6 +36,6 @@ for app in apps.get_app_configs():
|
||||
if not app.label.startswith("authentik"):
|
||||
continue
|
||||
for model in app.get_models():
|
||||
if not is_model_allowed(model):
|
||||
if not issubclass(model, SerializerModel):
|
||||
continue
|
||||
setattr(TestModels, f"test_{app.label}_{model.__name__}", serializer_tester_factory(model))
|
||||
|
@ -1082,6 +1082,12 @@ class AuthenticatedSession(SerializerModel):
|
||||
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
|
||||
@property
|
||||
def serializer(self) -> type[Serializer]:
|
||||
from authentik.core.api.authenticated_sessions import AuthenticatedSessionSerializer
|
||||
|
||||
return AuthenticatedSessionSerializer
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Authenticated Session")
|
||||
verbose_name_plural = _("Authenticated Sessions")
|
||||
|
@ -6,7 +6,7 @@ from djangoql.ast import Name
|
||||
from djangoql.exceptions import DjangoQLError
|
||||
from djangoql.queryset import apply_search
|
||||
from djangoql.schema import DjangoQLSchema
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.filters import BaseFilterBackend, SearchFilter
|
||||
from rest_framework.request import Request
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
@ -39,19 +39,21 @@ class BaseSchema(DjangoQLSchema):
|
||||
return super().resolve_name(name)
|
||||
|
||||
|
||||
class QLSearch(SearchFilter):
|
||||
class QLSearch(BaseFilterBackend):
|
||||
"""rest_framework search filter which uses DjangoQL"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._fallback = SearchFilter()
|
||||
|
||||
@property
|
||||
def enabled(self):
|
||||
return apps.get_app_config("authentik_enterprise").enabled()
|
||||
|
||||
def get_search_terms(self, request) -> str:
|
||||
"""
|
||||
Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited.
|
||||
"""
|
||||
params = request.query_params.get(self.search_param, "")
|
||||
def get_search_terms(self, request: Request) -> str:
|
||||
"""Search terms are set by a ?search=... query parameter,
|
||||
and may be comma and/or whitespace delimited."""
|
||||
params = request.query_params.get("search", "")
|
||||
params = params.replace("\x00", "") # strip null characters
|
||||
return params
|
||||
|
||||
@ -70,9 +72,9 @@ class QLSearch(SearchFilter):
|
||||
search_query = self.get_search_terms(request)
|
||||
schema = self.get_schema(request, view)
|
||||
if len(search_query) == 0 or not self.enabled:
|
||||
return super().filter_queryset(request, queryset, view)
|
||||
return self._fallback.filter_queryset(request, queryset, view)
|
||||
try:
|
||||
return apply_search(queryset, search_query, schema=schema)
|
||||
except DjangoQLError as exc:
|
||||
LOGGER.debug("Failed to parse search expression", exc=exc)
|
||||
return super().filter_queryset(request, queryset, view)
|
||||
return self._fallback.filter_queryset(request, queryset, view)
|
||||
|
@ -57,7 +57,7 @@ class QLTest(APITestCase):
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
content = loads(res.content)
|
||||
self.assertGreaterEqual(content["pagination"]["count"], 1)
|
||||
self.assertEqual(content["pagination"]["count"], 1)
|
||||
self.assertEqual(content["results"][0]["username"], self.user.username)
|
||||
|
||||
def test_search_json(self):
|
||||
|
@ -66,7 +66,10 @@ class RACClientConsumer(AsyncWebsocketConsumer):
|
||||
def init_outpost_connection(self):
|
||||
"""Initialize guac connection settings"""
|
||||
self.token = (
|
||||
ConnectionToken.filter_not_expired(token=self.scope["url_route"]["kwargs"]["token"])
|
||||
ConnectionToken.filter_not_expired(
|
||||
token=self.scope["url_route"]["kwargs"]["token"],
|
||||
session__session__session_key=self.scope["session"].session_key,
|
||||
)
|
||||
.select_related("endpoint", "provider", "session", "session__user")
|
||||
.first()
|
||||
)
|
||||
|
@ -87,3 +87,22 @@ class TestRACViews(APITestCase):
|
||||
)
|
||||
body = loads(flow_response.content)
|
||||
self.assertEqual(body["component"], "ak-stage-access-denied")
|
||||
|
||||
def test_different_session(self):
|
||||
"""Test request"""
|
||||
self.client.force_login(self.user)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_providers_rac:start",
|
||||
kwargs={"app": self.app.slug, "endpoint": str(self.endpoint.pk)},
|
||||
)
|
||||
)
|
||||
self.assertEqual(response.status_code, 302)
|
||||
flow_response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
body = loads(flow_response.content)
|
||||
next_url = body["to"]
|
||||
self.client.logout()
|
||||
final_response = self.client.get(next_url)
|
||||
self.assertEqual(final_response.url, reverse("authentik_core:if-user"))
|
||||
|
@ -68,7 +68,10 @@ class RACInterface(InterfaceView):
|
||||
|
||||
def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
|
||||
# Early sanity check to ensure token still exists
|
||||
token = ConnectionToken.filter_not_expired(token=self.kwargs["token"]).first()
|
||||
token = ConnectionToken.filter_not_expired(
|
||||
token=self.kwargs["token"],
|
||||
session__session__session_key=request.session.session_key,
|
||||
).first()
|
||||
if not token:
|
||||
return redirect("authentik_core:if-user")
|
||||
self.token = token
|
||||
|
@ -44,6 +44,7 @@ class RadiusProviderSerializer(ProviderSerializer):
|
||||
"shared_secret",
|
||||
"outpost_set",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||
|
||||
@ -79,6 +80,7 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
|
||||
"client_networks",
|
||||
"shared_secret",
|
||||
"mfa_support",
|
||||
"certificate",
|
||||
]
|
||||
|
||||
|
||||
|
@ -0,0 +1,25 @@
|
||||
# Generated by Django 5.1.9 on 2025-05-16 13:53
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||
("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="radiusprovider",
|
||||
name="certificate",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="authentik_crypto.certificatekeypair",
|
||||
),
|
||||
),
|
||||
]
|
@ -1,11 +1,14 @@
|
||||
"""Radius Provider"""
|
||||
|
||||
from collections.abc import Iterable
|
||||
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import Serializer
|
||||
|
||||
from authentik.core.models import PropertyMapping, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.lib.generators import generate_id
|
||||
from authentik.outposts.models import OutpostModel
|
||||
|
||||
@ -38,6 +41,10 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
),
|
||||
)
|
||||
|
||||
certificate = models.ForeignKey(
|
||||
CertificateKeyPair, on_delete=models.CASCADE, default=None, null=True
|
||||
)
|
||||
|
||||
@property
|
||||
def launch_url(self) -> str | None:
|
||||
"""Radius never has a launch URL"""
|
||||
@ -57,6 +64,12 @@ class RadiusProvider(OutpostModel, Provider):
|
||||
|
||||
return RadiusProviderSerializer
|
||||
|
||||
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||
required_models = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||
if self.certificate is not None:
|
||||
required_models.append(self.certificate)
|
||||
return required_models
|
||||
|
||||
def __str__(self):
|
||||
return f"Radius Provider {self.name}"
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@ -27,7 +27,6 @@
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<td>
|
||||
{% endblock %}
|
||||
|
||||
{% block sub_content %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema",
|
||||
"$id": "https://goauthentik.io/blueprints/schema.json",
|
||||
"type": "object",
|
||||
"title": "authentik 2025.6.2 Blueprint schema",
|
||||
"title": "authentik 2025.6.3 Blueprint schema",
|
||||
"required": [
|
||||
"version",
|
||||
"entries"
|
||||
@ -8953,6 +8953,11 @@
|
||||
"type": "boolean",
|
||||
"title": "MFA Support",
|
||||
"description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon."
|
||||
},
|
||||
"certificate": {
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"title": "Certificate"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
|
@ -31,7 +31,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -55,7 +55,7 @@ services:
|
||||
redis:
|
||||
condition: service_healthy
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.6.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
2
go.mod
2
go.mod
@ -29,7 +29,7 @@ require (
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/wwt/guac v1.3.2
|
||||
goauthentik.io/api/v3 v3.2025062.6
|
||||
goauthentik.io/api/v3 v3.2025063.1
|
||||
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
golang.org/x/sync v0.15.0
|
||||
|
4
go.sum
4
go.sum
@ -298,8 +298,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
goauthentik.io/api/v3 v3.2025062.6 h1:rlChhGP2vJufYCaTMb4sbRBEE1p2uL5T4HzMqF1AJ4A=
|
||||
goauthentik.io/api/v3 v3.2025062.6/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
goauthentik.io/api/v3 v3.2025063.1 h1:zvKhZTESgMY/SNiLuTs7G0YleBnev1v7+S9Xd6PZ9bc=
|
||||
goauthentik.io/api/v3 v3.2025063.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
|
@ -33,4 +33,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2025.6.2"
|
||||
const VERSION = "2025.6.3"
|
||||
|
@ -34,9 +34,10 @@ var (
|
||||
type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error)
|
||||
|
||||
type FlowExecutor struct {
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
InteractiveSolver SolverFunction
|
||||
|
||||
solvers map[StageComponent]SolverFunction
|
||||
|
||||
@ -94,6 +95,10 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
||||
return fe
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) AddHeader(name string, value string) {
|
||||
fe.api.GetConfig().AddDefaultHeader(name, value)
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
res, err := fe.transport.RoundTrip(req)
|
||||
if res != nil {
|
||||
@ -110,7 +115,7 @@ func (fe *FlowExecutor) ApiClient() *api.APIClient {
|
||||
return fe.api
|
||||
}
|
||||
|
||||
type challengeCommon interface {
|
||||
type ChallengeCommon interface {
|
||||
GetComponent() string
|
||||
GetResponseErrors() map[string][]api.ErrorDetail
|
||||
}
|
||||
@ -165,7 +170,7 @@ func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) {
|
||||
if i == nil {
|
||||
return nil, errors.New("response instance was null")
|
||||
}
|
||||
ch := i.(challengeCommon)
|
||||
ch := i.(ChallengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got challenge")
|
||||
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
gcsp.Finish()
|
||||
@ -184,7 +189,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response request instance was null")
|
||||
}
|
||||
ch := i.(challengeCommon)
|
||||
ch := i.(ChallengeCommon)
|
||||
|
||||
// Check for any validation errors that we might've gotten
|
||||
if len(ch.GetResponseErrors()) > 0 {
|
||||
@ -201,11 +206,17 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
case string(StageRedirect):
|
||||
return true, nil
|
||||
default:
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
var err error
|
||||
var rr api.FlowChallengeResponseRequest
|
||||
if fe.InteractiveSolver != nil {
|
||||
rr, err = fe.InteractiveSolver(challenge, responseReq)
|
||||
} else {
|
||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
rr, err = solver(challenge, responseReq)
|
||||
}
|
||||
rr, err := solver(challenge, responseReq)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@ -220,7 +231,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
||||
if i == nil {
|
||||
return false, errors.New("response instance was null")
|
||||
}
|
||||
ch = i.(challengeCommon)
|
||||
ch = i.(ChallengeCommon)
|
||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got response")
|
||||
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||
scsp.Finish()
|
||||
|
@ -8,6 +8,6 @@ import (
|
||||
)
|
||||
|
||||
func TestConvert(t *testing.T) {
|
||||
var a challengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
var a ChallengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||
assert.NotNil(t, a)
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func parseCIDRs(raw string) []*net.IPNet {
|
||||
@ -41,26 +42,28 @@ func (rs *RadiusServer) Refresh() error {
|
||||
if len(apiProviders) < 1 {
|
||||
return errors.New("no radius provider defined")
|
||||
}
|
||||
providers := make([]*ProviderInstance, len(apiProviders))
|
||||
for idx, provider := range apiProviders {
|
||||
providers := make(map[int32]*ProviderInstance)
|
||||
for _, provider := range apiProviders {
|
||||
existing, ok := rs.providers[provider.Pk]
|
||||
state := map[string]*protocol.State{}
|
||||
if ok {
|
||||
state = existing.eapState
|
||||
}
|
||||
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
|
||||
providers[idx] = &ProviderInstance{
|
||||
providers[provider.Pk] = &ProviderInstance{
|
||||
SharedSecret: []byte(provider.GetSharedSecret()),
|
||||
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
|
||||
MFASupport: provider.GetMfaSupport(),
|
||||
appSlug: provider.ApplicationSlug,
|
||||
flowSlug: provider.AuthFlowSlug,
|
||||
certId: provider.GetCertificate(),
|
||||
providerId: provider.Pk,
|
||||
s: rs,
|
||||
log: logger,
|
||||
eapState: state,
|
||||
}
|
||||
}
|
||||
rs.providers = providers
|
||||
rs.log.Info("Update providers")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) StartRadiusServer() error {
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
return rs.s.ListenAndServe()
|
||||
}
|
||||
|
44
internal/outpost/radius/eap/README.md
Normal file
44
internal/outpost/radius/eap/README.md
Normal file
@ -0,0 +1,44 @@
|
||||
# EAP protocol implementation
|
||||
|
||||
Install `eapol_test` (`sudo apt install eapoltest`)
|
||||
|
||||
Both PEAP and EAP-TLS require a minimal PKI setup. A CA, a certificate for the server and for EAP-TLS a client certificate need to be provided.
|
||||
|
||||
Save either of the config files below and run eapoltest like so:
|
||||
|
||||
```
|
||||
# peap.conf is the config file under the PEAP testing section
|
||||
# foo is the shared RADIUS secret
|
||||
# 1.2.3.4 is the IP of the RADIUS server
|
||||
eapol_test -c peap.conf -s foo -a 1.2.3.4
|
||||
```
|
||||
|
||||
### PEAP testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=PEAP
|
||||
identity="foo"
|
||||
password="bar"
|
||||
ca_cert="ca.pem"
|
||||
phase2="auth=MSCHAPV2"
|
||||
}
|
||||
```
|
||||
|
||||
### EAP-TLS testing
|
||||
|
||||
```
|
||||
network={
|
||||
ssid="DoesNotMatterForThisTest"
|
||||
key_mgmt=WPA-EAP
|
||||
eap=TLS
|
||||
identity="foo"
|
||||
ca_cert="ca.pem"
|
||||
client_cert="cert_client.pem"
|
||||
private_key="cert_client.key"
|
||||
eapol_flags=3
|
||||
eap_workaround=0
|
||||
}
|
||||
```
|
55
internal/outpost/radius/eap/context.go
Normal file
55
internal/outpost/radius/eap/context.go
Normal file
@ -0,0 +1,55 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type context struct {
|
||||
req *radius.Request
|
||||
rootPayload protocol.Payload
|
||||
typeState map[protocol.Type]any
|
||||
log *log.Entry
|
||||
settings interface{}
|
||||
parent *context
|
||||
endStatus protocol.Status
|
||||
handleInner func(protocol.Payload, protocol.StateManager, protocol.Context) (protocol.Payload, error)
|
||||
}
|
||||
|
||||
func (ctx *context) RootPayload() protocol.Payload { return ctx.rootPayload }
|
||||
func (ctx *context) Packet() *radius.Request { return ctx.req }
|
||||
func (ctx *context) ProtocolSettings() any { return ctx.settings }
|
||||
func (ctx *context) GetProtocolState(p protocol.Type) any { return ctx.typeState[p] }
|
||||
func (ctx *context) SetProtocolState(p protocol.Type, st any) { ctx.typeState[p] = st }
|
||||
func (ctx *context) IsProtocolStart(p protocol.Type) bool { return ctx.typeState[p] == nil }
|
||||
func (ctx *context) Log() *log.Entry { return ctx.log }
|
||||
func (ctx *context) HandleInnerEAP(p protocol.Payload, st protocol.StateManager) (protocol.Payload, error) {
|
||||
return ctx.handleInner(p, st, ctx)
|
||||
}
|
||||
func (ctx *context) Inner(p protocol.Payload, t protocol.Type) protocol.Context {
|
||||
nctx := &context{
|
||||
req: ctx.req,
|
||||
rootPayload: ctx.rootPayload,
|
||||
typeState: ctx.typeState,
|
||||
log: ctx.log.WithField("type", fmt.Sprintf("%T", p)).WithField("code", t),
|
||||
settings: ctx.settings,
|
||||
parent: ctx,
|
||||
handleInner: ctx.handleInner,
|
||||
}
|
||||
nctx.log.Debug("Creating inner context")
|
||||
return nctx
|
||||
}
|
||||
func (ctx *context) EndInnerProtocol(st protocol.Status) {
|
||||
ctx.log.Info("Ending protocol")
|
||||
if ctx.parent != nil {
|
||||
ctx.parent.EndInnerProtocol(st)
|
||||
return
|
||||
}
|
||||
if ctx.endStatus != protocol.StatusUnknown {
|
||||
return
|
||||
}
|
||||
ctx.endStatus = st
|
||||
}
|
13
internal/outpost/radius/eap/debug/debug.go
Normal file
13
internal/outpost/radius/eap/debug/debug.go
Normal file
@ -0,0 +1,13 @@
|
||||
package debug
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FormatBytes(d []byte) string {
|
||||
b := d
|
||||
if len(b) > 32 {
|
||||
b = b[:32]
|
||||
}
|
||||
return fmt.Sprintf("% x", b)
|
||||
}
|
182
internal/outpost/radius/eap/handler.go
Normal file
182
internal/outpost/radius/eap/handler.go
Normal file
@ -0,0 +1,182 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func sendErrorResponse(w radius.ResponseWriter, r *radius.Request) {
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
err := w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) HandleRadiusPacket(w radius.ResponseWriter, r *radius.Request) {
|
||||
p.r = r
|
||||
rst := rfc2865.State_GetString(r.Packet)
|
||||
if rst == "" {
|
||||
rst = base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(12))
|
||||
}
|
||||
p.state = rst
|
||||
|
||||
rp := &Packet{r: r}
|
||||
rep, err := p.handleEAP(p.eap, p.stm, nil)
|
||||
rp.eap = rep
|
||||
|
||||
rres := r.Response(radius.CodeAccessReject)
|
||||
if err == nil {
|
||||
switch rp.eap.Code {
|
||||
case protocol.CodeRequest:
|
||||
rres.Code = radius.CodeAccessChallenge
|
||||
case protocol.CodeFailure:
|
||||
rres.Code = radius.CodeAccessReject
|
||||
case protocol.CodeSuccess:
|
||||
rres.Code = radius.CodeAccessAccept
|
||||
}
|
||||
} else {
|
||||
rres.Code = radius.CodeAccessReject
|
||||
log.WithError(err).Debug("Rejecting request")
|
||||
}
|
||||
for _, mod := range p.responseModifiers {
|
||||
err := mod.ModifyRADIUSResponse(rres, r.Packet)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Root-EAP: failed to modify response packet")
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
rfc2865.State_SetString(rres, p.state)
|
||||
eapEncoded, err := rp.Encode()
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to encode response")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
log.WithField("length", len(eapEncoded)).WithField("type", fmt.Sprintf("%T", rp.eap.Payload)).Debug("Root-EAP: encapsulated challenge")
|
||||
rfc2869.EAPMessage_Set(rres, eapEncoded)
|
||||
err = p.setMessageAuthenticator(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send message authenticator")
|
||||
sendErrorResponse(w, r)
|
||||
return
|
||||
}
|
||||
err = w.Write(rres)
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("failed to send response")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Packet) handleEAP(pp protocol.Payload, stm protocol.StateManager, parentContext *context) (*eap.Payload, error) {
|
||||
st := stm.GetEAPState(p.state)
|
||||
if st == nil {
|
||||
log.Debug("Root-EAP: blank state")
|
||||
st = protocol.BlankState(stm.GetEAPSettings())
|
||||
}
|
||||
|
||||
nextChallengeToOffer, err := st.GetNextProtocol()
|
||||
if err != nil {
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: p.eap.ID,
|
||||
}, err
|
||||
}
|
||||
|
||||
next := func() (*eap.Payload, error) {
|
||||
st.ProtocolIndex += 1
|
||||
st.TypeState = map[protocol.Type]any{}
|
||||
stm.SetEAPState(p.state, st)
|
||||
return p.handleEAP(pp, stm, nil)
|
||||
}
|
||||
|
||||
if n, ok := pp.(*eap.Payload).Payload.(*legacy_nak.Payload); ok {
|
||||
log.WithField("desired", n.DesiredType).Debug("Root-EAP: received NAK, trying next protocol")
|
||||
pp.(*eap.Payload).Payload = nil
|
||||
return next()
|
||||
}
|
||||
|
||||
np, t, _ := eap.EmptyPayload(stm.GetEAPSettings(), nextChallengeToOffer)
|
||||
|
||||
var ctx *context
|
||||
if parentContext != nil {
|
||||
ctx = parentContext.Inner(np, t).(*context)
|
||||
ctx.settings = stm.GetEAPSettings().ProtocolSettings[np.Type()]
|
||||
} else {
|
||||
ctx = &context{
|
||||
req: p.r,
|
||||
rootPayload: p.eap,
|
||||
typeState: st.TypeState,
|
||||
log: log.WithField("type", fmt.Sprintf("%T", np)).WithField("code", t),
|
||||
settings: stm.GetEAPSettings().ProtocolSettings[t],
|
||||
}
|
||||
ctx.handleInner = func(pp protocol.Payload, sm protocol.StateManager, ctx protocol.Context) (protocol.Payload, error) {
|
||||
// cctx := ctx.Inner(np, np.Type(), nil).(*context)
|
||||
return p.handleEAP(pp, sm, ctx.(*context))
|
||||
}
|
||||
}
|
||||
if !np.Offerable() {
|
||||
ctx.Log().Debug("Root-EAP: protocol not offerable, skipping")
|
||||
return next()
|
||||
}
|
||||
ctx.Log().Debug("Root-EAP: Passing to protocol")
|
||||
|
||||
res := &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: p.eap.ID + 1,
|
||||
MsgType: t,
|
||||
}
|
||||
var payload any
|
||||
if reflect.TypeOf(pp.(*eap.Payload).Payload) == reflect.TypeOf(np) {
|
||||
np.Decode(pp.(*eap.Payload).RawPayload)
|
||||
}
|
||||
payload = np.Handle(ctx)
|
||||
if payload != nil {
|
||||
res.Payload = payload.(protocol.Payload)
|
||||
}
|
||||
|
||||
stm.SetEAPState(p.state, st)
|
||||
|
||||
if rm, ok := np.(protocol.ResponseModifier); ok {
|
||||
ctx.log.Debug("Root-EAP: Registered response modifier")
|
||||
p.responseModifiers = append(p.responseModifiers, rm)
|
||||
}
|
||||
|
||||
switch ctx.endStatus {
|
||||
case protocol.StatusSuccess:
|
||||
res.Code = protocol.CodeSuccess
|
||||
res.ID -= 1
|
||||
case protocol.StatusError:
|
||||
res.Code = protocol.CodeFailure
|
||||
res.ID -= 1
|
||||
case protocol.StatusNextProtocol:
|
||||
ctx.log.Debug("Root-EAP: Protocol ended, starting next protocol")
|
||||
return next()
|
||||
case protocol.StatusUnknown:
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (p *Packet) setMessageAuthenticator(rp *radius.Packet) error {
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
|
||||
hash := hmac.New(md5.New, rp.Secret)
|
||||
encode, err := rp.MarshalBinary()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash.Write(encode)
|
||||
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
|
||||
return nil
|
||||
}
|
34
internal/outpost/radius/eap/packet.go
Normal file
34
internal/outpost/radius/eap/packet.go
Normal file
@ -0,0 +1,34 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Packet struct {
|
||||
r *radius.Request
|
||||
eap *eap.Payload
|
||||
stm protocol.StateManager
|
||||
state string
|
||||
responseModifiers []protocol.ResponseModifier
|
||||
}
|
||||
|
||||
func Decode(stm protocol.StateManager, raw []byte) (*Packet, error) {
|
||||
packet := &Packet{
|
||||
eap: &eap.Payload{
|
||||
Settings: stm.GetEAPSettings(),
|
||||
},
|
||||
stm: stm,
|
||||
responseModifiers: []protocol.ResponseModifier{},
|
||||
}
|
||||
err := packet.eap.Decode(raw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return packet, nil
|
||||
}
|
||||
|
||||
func (p *Packet) Encode() ([]byte, error) {
|
||||
return p.eap.Encode()
|
||||
}
|
32
internal/outpost/radius/eap/protocol/context.go
Normal file
32
internal/outpost/radius/eap/protocol/context.go
Normal file
@ -0,0 +1,32 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"layeh.com/radius"
|
||||
)
|
||||
|
||||
type Status int
|
||||
|
||||
const (
|
||||
StatusUnknown Status = iota
|
||||
StatusSuccess
|
||||
StatusError
|
||||
StatusNextProtocol
|
||||
)
|
||||
|
||||
type Context interface {
|
||||
Packet() *radius.Request
|
||||
RootPayload() Payload
|
||||
|
||||
ProtocolSettings() interface{}
|
||||
|
||||
GetProtocolState(p Type) interface{}
|
||||
SetProtocolState(p Type, s interface{})
|
||||
IsProtocolStart(p Type) bool
|
||||
|
||||
HandleInnerEAP(Payload, StateManager) (Payload, error)
|
||||
Inner(Payload, Type) Context
|
||||
EndInnerProtocol(Status)
|
||||
|
||||
Log() *log.Entry
|
||||
}
|
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
@ -0,0 +1,23 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func EmptyPayload(settings protocol.Settings, t protocol.Type) (protocol.Payload, protocol.Type, error) {
|
||||
for _, cons := range settings.Protocols {
|
||||
np := cons()
|
||||
if np.Type() == t {
|
||||
return np, np.Type(), nil
|
||||
}
|
||||
// If the protocol has an inner protocol, return the original type but the code for the inner protocol
|
||||
if i, ok := np.(protocol.Inner); ok {
|
||||
if ii := i.HasInner(); ii != nil {
|
||||
return np, ii.Type(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, protocol.Type(0), fmt.Errorf("unsupported EAP type %d", t)
|
||||
}
|
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
@ -0,0 +1,96 @@
|
||||
package eap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeEAP protocol.Type = 0
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Code protocol.Code
|
||||
ID uint8
|
||||
Length uint16
|
||||
MsgType protocol.Type
|
||||
Payload protocol.Payload
|
||||
RawPayload []byte
|
||||
|
||||
Settings protocol.Settings
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeEAP
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Code = protocol.Code(raw[0])
|
||||
p.ID = raw[1]
|
||||
p.Length = binary.BigEndian.Uint16(raw[2:])
|
||||
if p.Length != uint16(len(raw)) {
|
||||
return fmt.Errorf("mismatched packet length; got %d, expected %d", p.Length, uint16(len(raw)))
|
||||
}
|
||||
if len(raw) > 4 && (p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse) {
|
||||
p.MsgType = protocol.Type(raw[4])
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Trace("EAP: decode raw")
|
||||
p.RawPayload = raw[5:]
|
||||
if p.Payload == nil {
|
||||
pp, _, err := EmptyPayload(p.Settings, p.MsgType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Payload = pp
|
||||
}
|
||||
err := p.Payload.Decode(raw[5:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
buff := make([]byte, 4)
|
||||
buff[0] = uint8(p.Code)
|
||||
buff[1] = uint8(p.ID)
|
||||
|
||||
if p.Payload != nil {
|
||||
payloadBuffer, err := p.Payload.Encode()
|
||||
if err != nil {
|
||||
return buff, err
|
||||
}
|
||||
if p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse {
|
||||
buff = append(buff, uint8(p.MsgType))
|
||||
}
|
||||
buff = append(buff, payloadBuffer...)
|
||||
}
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(buff)))
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
ctx.Log().Debug("EAP: Handle")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<EAP Packet Code=%d, ID=%d, Type=%d, Length=%d, Payload=%T>",
|
||||
p.Code,
|
||||
p.ID,
|
||||
p.MsgType,
|
||||
p.Length,
|
||||
p.Payload,
|
||||
)
|
||||
}
|
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
@ -0,0 +1,5 @@
|
||||
package eap
|
||||
|
||||
type State struct {
|
||||
PacketID uint8
|
||||
}
|
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
@ -0,0 +1,61 @@
|
||||
package gtc
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeGTC protocol.Type = 6
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Challenge []byte
|
||||
|
||||
st *State
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeGTC
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return p.Challenge, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeGTC, p.st)
|
||||
}()
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
if ctx.IsProtocolStart(TypeGTC) {
|
||||
g, v := settings.ChallengeHandler(ctx)
|
||||
p.st = &State{
|
||||
getChallenge: g,
|
||||
validateResponse: v,
|
||||
}
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeGTC).(*State)
|
||||
p.st.validateResponse(p.raw)
|
||||
return &Payload{
|
||||
Challenge: p.st.getChallenge(),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return "<GTC Packet>"
|
||||
}
|
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
@ -0,0 +1,10 @@
|
||||
package gtc
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type GetChallenge func() []byte
|
||||
type ValidateResponse func(answer []byte)
|
||||
|
||||
type Settings struct {
|
||||
ChallengeHandler func(ctx protocol.Context) (GetChallenge, ValidateResponse)
|
||||
}
|
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
@ -0,0 +1,6 @@
|
||||
package gtc
|
||||
|
||||
type State struct {
|
||||
getChallenge GetChallenge
|
||||
validateResponse ValidateResponse
|
||||
}
|
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
@ -0,0 +1,48 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeIdentity protocol.Type = 1
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Identity string
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeIdentity
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Identity = string(raw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeIdentity) {
|
||||
ctx.EndInnerProtocol(protocol.StatusNextProtocol)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Identity Packet Identity=%s>",
|
||||
p.Identity,
|
||||
)
|
||||
}
|
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
@ -0,0 +1,48 @@
|
||||
package legacy_nak
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypeLegacyNAK protocol.Type = 3
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
DesiredType protocol.Type
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeLegacyNAK
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.DesiredType = protocol.Type(raw[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
return []byte{byte(p.DesiredType)}, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
if ctx.IsProtocolStart(TypeLegacyNAK) {
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Legacy NAK Packet DesiredType=%d>",
|
||||
p.DesiredType,
|
||||
)
|
||||
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
@ -0,0 +1,23 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Challenge []byte
|
||||
NTResponse []byte
|
||||
Flags uint8
|
||||
}
|
||||
|
||||
func ParseResponse(raw []byte) (*Response, error) {
|
||||
res := &Response{}
|
||||
res.Challenge = raw[:challengeValueSize]
|
||||
if !bytes.Equal(raw[challengeValueSize:challengeValueSize+responseReservedSize], make([]byte, 8)) {
|
||||
return nil, errors.New("MSCHAPv2: Reserved bytes not empty?")
|
||||
}
|
||||
res.NTResponse = raw[challengeValueSize+responseReservedSize : challengeValueSize+responseReservedSize+responseNTResponseSize]
|
||||
res.Flags = (raw[challengeValueSize+responseReservedSize+responseNTResponseSize])
|
||||
return res, nil
|
||||
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
@ -0,0 +1,23 @@
|
||||
package mschapv2
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
type SuccessRequest struct {
|
||||
*Payload
|
||||
Authenticator []byte
|
||||
}
|
||||
|
||||
// A success request is encoded slightly differently, it doesn't have a challenge and as such
|
||||
// doesn't need to encode the length of it
|
||||
func (sr *SuccessRequest) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(sr.OpCode),
|
||||
sr.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
encoded = append(encoded, sr.Authenticator...)
|
||||
sr.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], sr.MSLength)
|
||||
return encoded, nil
|
||||
}
|
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
@ -0,0 +1,196 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const TypeMSCHAPv2 protocol.Type = 26
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
const (
|
||||
challengeValueSize = 16
|
||||
responseValueSize = 49
|
||||
responseReservedSize = 8
|
||||
responseNTResponseSize = 24
|
||||
)
|
||||
|
||||
type OpCode uint8
|
||||
|
||||
const (
|
||||
OpChallenge OpCode = 1
|
||||
OpResponse OpCode = 2
|
||||
OpSuccess OpCode = 3
|
||||
)
|
||||
|
||||
type Payload struct {
|
||||
OpCode OpCode
|
||||
MSCHAPv2ID uint8
|
||||
MSLength uint16
|
||||
ValueSize uint8
|
||||
|
||||
Challenge []byte
|
||||
Response []byte
|
||||
|
||||
Name []byte
|
||||
|
||||
st *State
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeMSCHAPv2
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("MSCHAPv2: decode raw")
|
||||
p.OpCode = OpCode(raw[0])
|
||||
if p.OpCode == OpSuccess {
|
||||
return nil
|
||||
}
|
||||
// TODO: Validate against root EAP packet
|
||||
p.MSCHAPv2ID = raw[1]
|
||||
p.MSLength = binary.BigEndian.Uint16(raw[2:])
|
||||
|
||||
p.ValueSize = raw[4]
|
||||
if p.ValueSize != responseValueSize {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect value size: %d", p.ValueSize)
|
||||
}
|
||||
p.Response = raw[5 : p.ValueSize+5]
|
||||
p.Name = raw[5+p.ValueSize:]
|
||||
if int(p.MSLength) != len(raw) {
|
||||
return fmt.Errorf("MSCHAPv2: incorrect MS-Length: %d, should be %d", p.MSLength, len(raw))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
encoded := []byte{
|
||||
byte(p.OpCode),
|
||||
p.MSCHAPv2ID,
|
||||
0,
|
||||
0,
|
||||
byte(len(p.Challenge)),
|
||||
}
|
||||
encoded = append(encoded, p.Challenge...)
|
||||
encoded = append(encoded, p.Name...)
|
||||
p.MSLength = uint16(len(encoded))
|
||||
binary.BigEndian.PutUint16(encoded[2:], p.MSLength)
|
||||
return encoded, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeMSCHAPv2, p.st)
|
||||
}()
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypeMSCHAPv2) {
|
||||
ctx.Log().Debug("MSCHAPv2: Empty state, starting")
|
||||
p.st = &State{
|
||||
Challenge: securecookie.GenerateRandomKey(challengeValueSize),
|
||||
}
|
||||
return &Payload{
|
||||
OpCode: OpChallenge,
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
Challenge: p.st.Challenge,
|
||||
Name: []byte("authentik"),
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeMSCHAPv2).(*State)
|
||||
|
||||
response := &Payload{
|
||||
MSCHAPv2ID: rootEap.ID + 1,
|
||||
}
|
||||
|
||||
settings := ctx.ProtocolSettings().(Settings)
|
||||
|
||||
ctx.Log().Debugf("MSCHAPv2: OpCode: %d", p.OpCode)
|
||||
if p.OpCode == OpResponse {
|
||||
res, err := ParseResponse(p.Response)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to parse response")
|
||||
return nil
|
||||
}
|
||||
p.st.PeerChallenge = res.Challenge
|
||||
auth, err := settings.AuthenticateRequest(AuthRequest{
|
||||
Challenge: p.st.Challenge,
|
||||
PeerChallenge: p.st.PeerChallenge,
|
||||
})
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to check password")
|
||||
return nil
|
||||
}
|
||||
if !bytes.Equal(auth.NTResponse, res.NTResponse) {
|
||||
ctx.Log().Warning("MSCHAPv2: NT response mismatch")
|
||||
return nil
|
||||
}
|
||||
ctx.Log().Info("MSCHAPv2: Successfully checked password")
|
||||
p.st.AuthResponse = auth
|
||||
succ := &SuccessRequest{
|
||||
Payload: &Payload{
|
||||
OpCode: OpSuccess,
|
||||
},
|
||||
Authenticator: []byte(auth.AuthenticatorResponse),
|
||||
}
|
||||
return succ
|
||||
} else if p.OpCode == OpSuccess && p.st.AuthResponse != nil {
|
||||
ep := &peap.ExtensionPayload{
|
||||
AVPs: []peap.ExtensionAVP{
|
||||
{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPAckResult,
|
||||
Value: []byte{0, 1},
|
||||
},
|
||||
},
|
||||
}
|
||||
p.st.IsProtocolEnded = true
|
||||
return ep
|
||||
} else if p.st.IsProtocolEnded {
|
||||
ctx.EndInnerProtocol(protocol.StatusSuccess)
|
||||
return &Payload{}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if p.st == nil || p.st.AuthResponse == nil {
|
||||
return nil
|
||||
}
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
log.Debug("MSCHAPv2: Radius modifier")
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.AuthResponse.RecvKey)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) < 1 {
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.AuthResponse.SendKey)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<MSCHAPv2 Packet OpCode=%d, MSCHAPv2ID=%d>",
|
||||
p.OpCode,
|
||||
p.MSCHAPv2ID,
|
||||
)
|
||||
}
|
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
@ -0,0 +1,50 @@
|
||||
package mschapv2
|
||||
|
||||
import (
|
||||
"layeh.com/radius/rfc2759"
|
||||
"layeh.com/radius/rfc3079"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
AuthenticateRequest func(req AuthRequest) (*AuthResponse, error)
|
||||
}
|
||||
|
||||
type AuthRequest struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
}
|
||||
|
||||
type AuthResponse struct {
|
||||
NTResponse []byte
|
||||
RecvKey []byte
|
||||
SendKey []byte
|
||||
AuthenticatorResponse string
|
||||
}
|
||||
|
||||
func DebugStaticCredentials(user, password []byte) func(req AuthRequest) (*AuthResponse, error) {
|
||||
return func(req AuthRequest) (*AuthResponse, error) {
|
||||
res := &AuthResponse{}
|
||||
ntResponse, err := rfc2759.GenerateNTResponse(req.Challenge, req.PeerChallenge, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.NTResponse = ntResponse
|
||||
|
||||
res.RecvKey, err = rfc3079.MakeKey(ntResponse, password, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.SendKey, err = rfc3079.MakeKey(ntResponse, password, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.AuthenticatorResponse, err = rfc2759.GenerateAuthenticatorResponse(req.Challenge, req.PeerChallenge, ntResponse, user, password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return res, nil
|
||||
|
||||
}
|
||||
}
|
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
@ -0,0 +1,8 @@
|
||||
package mschapv2
|
||||
|
||||
type State struct {
|
||||
Challenge []byte
|
||||
PeerChallenge []byte
|
||||
IsProtocolEnded bool
|
||||
AuthResponse *AuthResponse
|
||||
}
|
31
internal/outpost/radius/eap/protocol/packet.go
Normal file
31
internal/outpost/radius/eap/protocol/packet.go
Normal file
@ -0,0 +1,31 @@
|
||||
package protocol
|
||||
|
||||
import "layeh.com/radius"
|
||||
|
||||
type Type uint8
|
||||
|
||||
type Code uint8
|
||||
|
||||
const (
|
||||
CodeRequest Code = 1
|
||||
CodeResponse Code = 2
|
||||
CodeSuccess Code = 3
|
||||
CodeFailure Code = 4
|
||||
)
|
||||
|
||||
type Payload interface {
|
||||
Decode(raw []byte) error
|
||||
Encode() ([]byte, error)
|
||||
Handle(ctx Context) Payload
|
||||
Type() Type
|
||||
Offerable() bool
|
||||
String() string
|
||||
}
|
||||
|
||||
type Inner interface {
|
||||
HasInner() Payload
|
||||
}
|
||||
|
||||
type ResponseModifier interface {
|
||||
ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error
|
||||
}
|
59
internal/outpost/radius/eap/protocol/peap/extension.go
Normal file
59
internal/outpost/radius/eap/protocol/peap/extension.go
Normal file
@ -0,0 +1,59 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
const TypePEAPExtension protocol.Type = 33
|
||||
|
||||
type ExtensionPayload struct {
|
||||
AVPs []ExtensionAVP
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debugf("PEAP-Extension: decode raw")
|
||||
ep.AVPs = []ExtensionAVP{}
|
||||
offset := 0
|
||||
for {
|
||||
if len(raw[offset:]) < 4 {
|
||||
return nil
|
||||
}
|
||||
len := binary.BigEndian.Uint16(raw[offset+2:offset+2+2]) + ExtensionHeaderSize
|
||||
avp := &ExtensionAVP{}
|
||||
err := avp.Decode(raw[offset : offset+int(len)])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ep.AVPs = append(ep.AVPs, *avp)
|
||||
offset = offset + int(len)
|
||||
}
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Encode() ([]byte, error) {
|
||||
log.Debug("PEAP-Extension: encode")
|
||||
buff := []byte{}
|
||||
for _, avp := range ep.AVPs {
|
||||
buff = append(buff, avp.Encode()...)
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Handle(protocol.Context) protocol.Payload {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Offerable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) String() string {
|
||||
return "<PEAP Extension Payload>"
|
||||
}
|
||||
|
||||
func (ep *ExtensionPayload) Type() protocol.Type {
|
||||
return TypePEAPExtension
|
||||
}
|
62
internal/outpost/radius/eap/protocol/peap/extension_avp.go
Normal file
62
internal/outpost/radius/eap/protocol/peap/extension_avp.go
Normal file
@ -0,0 +1,62 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AVPType uint16
|
||||
|
||||
const (
|
||||
AVPAckResult AVPType = 3
|
||||
)
|
||||
|
||||
const ExtensionHeaderSize = 4
|
||||
|
||||
type ExtensionAVP struct {
|
||||
Mandatory bool
|
||||
Type AVPType // 14-bit field
|
||||
Length uint16
|
||||
Value []byte
|
||||
}
|
||||
|
||||
var (
|
||||
ErrorReservedBitSet = errors.New("PEAP-Extension: Reserved bit is not 0")
|
||||
)
|
||||
|
||||
func (eavp *ExtensionAVP) Decode(raw []byte) error {
|
||||
typ := binary.BigEndian.Uint16(raw[:2])
|
||||
if typ>>15 == 1 {
|
||||
eavp.Mandatory = true
|
||||
}
|
||||
if typ>>14&1 != 0 {
|
||||
return ErrorReservedBitSet
|
||||
}
|
||||
eavp.Type = AVPType(typ & 0b0011111111111111)
|
||||
eavp.Length = binary.BigEndian.Uint16(raw[2:4])
|
||||
val := raw[4:]
|
||||
if eavp.Length != uint16(len(val)) {
|
||||
return fmt.Errorf("PEAP-Extension: Invalid length: %d, should be %d", eavp.Length, len(val))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (eavp ExtensionAVP) Encode() []byte {
|
||||
buff := []byte{
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
}
|
||||
t := uint16(eavp.Type)
|
||||
// Type is a 14-bit number, the highest bit is the mandatory flag
|
||||
if eavp.Mandatory {
|
||||
t = t | 0b1000000000000000
|
||||
}
|
||||
// The next bit is reserved and should always be set to 0
|
||||
t = t & 0b1011111111111111
|
||||
binary.BigEndian.PutUint16(buff[0:], t)
|
||||
binary.BigEndian.PutUint16(buff[2:], uint16(len(eavp.Value)))
|
||||
return append(buff, eavp.Value...)
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package peap_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
)
|
||||
|
||||
func TestEncode(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{
|
||||
Mandatory: true,
|
||||
Type: peap.AVPType(3),
|
||||
}
|
||||
assert.Equal(t, []byte{0x80, 0x3, 0x0, 0x0}, eavp.Encode())
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, eavp.Mandatory)
|
||||
assert.Equal(t, peap.AVPType(3), eavp.Type)
|
||||
}
|
||||
|
||||
func TestDecode_Invalid_ReservedBitSet(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0xc0, 0x3, 0x0, 0x0})
|
||||
assert.ErrorIs(t, err, peap.ErrorReservedBitSet)
|
||||
}
|
||||
|
||||
func TestDecode_Invalid_Length(t *testing.T) {
|
||||
eavp := peap.ExtensionAVP{}
|
||||
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0, 0x0})
|
||||
assert.NotNil(t, err)
|
||||
}
|
167
internal/outpost/radius/eap/protocol/peap/payload.go
Normal file
167
internal/outpost/radius/eap/protocol/peap/payload.go
Normal file
@ -0,0 +1,167 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||
)
|
||||
|
||||
const TypePEAP protocol.Type = 25
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &tls.Payload{
|
||||
Inner: &Payload{
|
||||
Inner: &eap.Payload{},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Inner protocol.Payload
|
||||
|
||||
eap *eap.Payload
|
||||
st *State
|
||||
settings Settings
|
||||
raw []byte
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypePEAP
|
||||
}
|
||||
|
||||
func (p *Payload) HasInner() protocol.Payload {
|
||||
return p.Inner
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
log.WithField("raw", debug.FormatBytes(raw)).Debug("PEAP: Decode")
|
||||
p.raw = raw
|
||||
return nil
|
||||
}
|
||||
|
||||
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
log.Debug("PEAP: Encoding inner EAP")
|
||||
if p.eap.Payload == nil {
|
||||
return []byte{}, errors.New("PEAP: no payload in response eap packet")
|
||||
}
|
||||
payload, err := p.eap.Payload.Encode()
|
||||
if err != nil {
|
||||
return []byte{}, err
|
||||
}
|
||||
encoded := []byte{
|
||||
byte(p.eap.MsgType),
|
||||
}
|
||||
return append(encoded, payload...), nil
|
||||
}
|
||||
|
||||
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||
func (p *Payload) eapInnerDecode(ctx protocol.Context) (*eap.Payload, error) {
|
||||
ep := &eap.Payload{
|
||||
Settings: p.GetEAPSettings(),
|
||||
}
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
fixedRaw := []byte{
|
||||
byte(rootEap.Code),
|
||||
rootEap.ID,
|
||||
// 2 byte space for length
|
||||
0,
|
||||
0,
|
||||
}
|
||||
fullLength := len(p.raw) + len(fixedRaw)
|
||||
binary.BigEndian.PutUint16(fixedRaw[2:], uint16(fullLength))
|
||||
fixedRaw = append(fixedRaw, p.raw...)
|
||||
// If the raw data has a msgtype set to type 33 (EAP extension), decode differently
|
||||
if len(p.raw) > 5 && p.raw[4] == byte(TypePEAPExtension) {
|
||||
ep.Payload = &ExtensionPayload{}
|
||||
// Pass original raw data to EAP as extension payloads are encoded like normal EAP packets
|
||||
fixedRaw = p.raw
|
||||
}
|
||||
err := ep.Decode(fixedRaw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ep, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypePEAP, p.st)
|
||||
}()
|
||||
p.settings = ctx.ProtocolSettings().(Settings)
|
||||
|
||||
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||
|
||||
if ctx.IsProtocolStart(TypePEAP) {
|
||||
ctx.Log().Debug("PEAP: Protocol start")
|
||||
p.st = &State{
|
||||
SubState: make(map[string]*protocol.State),
|
||||
}
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeRequest,
|
||||
ID: rootEap.ID + 1,
|
||||
MsgType: identity.TypeIdentity,
|
||||
Payload: &identity.Payload{},
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypePEAP).(*State)
|
||||
|
||||
ep, err := p.eapInnerDecode(ctx)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("PEAP: failed to decode inner EAP")
|
||||
return &eap.Payload{
|
||||
Code: protocol.CodeFailure,
|
||||
ID: rootEap.ID + 1,
|
||||
}
|
||||
}
|
||||
p.eap = ep
|
||||
ctx.Log().Debugf("PEAP: Decoded inner EAP to %s", ep.String())
|
||||
|
||||
res, err := ctx.HandleInnerEAP(ep, p)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("PEAP: failed to handle inner EAP")
|
||||
return nil
|
||||
}
|
||||
// Normal payloads need to be wrapped in PEAP to use the correct encoding (see Encode() above)
|
||||
// Extension payloads handle encoding differently
|
||||
pres := res.(*eap.Payload)
|
||||
if _, ok := pres.Payload.(*ExtensionPayload); ok {
|
||||
// HandleInnerEAP will set the MsgType to the PEAP type, however we need to override that
|
||||
pres.MsgType = TypePEAPExtension
|
||||
ctx.Log().Debug("PEAP: Encoding response as extension")
|
||||
return res
|
||||
}
|
||||
return &Payload{eap: pres}
|
||||
}
|
||||
|
||||
func (p *Payload) GetEAPSettings() protocol.Settings {
|
||||
return p.settings.InnerProtocols
|
||||
}
|
||||
|
||||
func (p *Payload) GetEAPState(key string) *protocol.State {
|
||||
return p.st.SubState[key]
|
||||
}
|
||||
|
||||
func (p *Payload) SetEAPState(key string, st *protocol.State) {
|
||||
p.st.SubState[key] = st
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<PEAP Packet Wrapping=%s>",
|
||||
p.eap.String(),
|
||||
)
|
||||
}
|
16
internal/outpost/radius/eap/protocol/peap/settings.go
Normal file
16
internal/outpost/radius/eap/protocol/peap/settings.go
Normal file
@ -0,0 +1,16 @@
|
||||
package peap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Config *tls.Config
|
||||
InnerProtocols protocol.Settings
|
||||
}
|
||||
|
||||
func (s Settings) TLSConfig() *tls.Config {
|
||||
return s.Config
|
||||
}
|
7
internal/outpost/radius/eap/protocol/peap/state.go
Normal file
7
internal/outpost/radius/eap/protocol/peap/state.go
Normal file
@ -0,0 +1,7 @@
|
||||
package peap
|
||||
|
||||
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
|
||||
type State struct {
|
||||
SubState map[string]*protocol.State
|
||||
}
|
42
internal/outpost/radius/eap/protocol/state.go
Normal file
42
internal/outpost/radius/eap/protocol/state.go
Normal file
@ -0,0 +1,42 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"slices"
|
||||
)
|
||||
|
||||
type StateManager interface {
|
||||
GetEAPSettings() Settings
|
||||
GetEAPState(string) *State
|
||||
SetEAPState(string, *State)
|
||||
}
|
||||
|
||||
type ProtocolConstructor func() Payload
|
||||
|
||||
type Settings struct {
|
||||
Protocols []ProtocolConstructor
|
||||
ProtocolPriority []Type
|
||||
ProtocolSettings map[Type]interface{}
|
||||
}
|
||||
|
||||
type State struct {
|
||||
Protocols []ProtocolConstructor
|
||||
ProtocolIndex int
|
||||
ProtocolPriority []Type
|
||||
TypeState map[Type]any
|
||||
}
|
||||
|
||||
func (st *State) GetNextProtocol() (Type, error) {
|
||||
if st.ProtocolIndex >= len(st.ProtocolPriority) {
|
||||
return Type(0), errors.New("no more protocols to offer")
|
||||
}
|
||||
return st.ProtocolPriority[st.ProtocolIndex], nil
|
||||
}
|
||||
|
||||
func BlankState(settings Settings) *State {
|
||||
return &State{
|
||||
Protocols: slices.Clone(settings.Protocols),
|
||||
ProtocolPriority: slices.Clone(settings.ProtocolPriority),
|
||||
TypeState: map[Type]any{},
|
||||
}
|
||||
}
|
111
internal/outpost/radius/eap/protocol/tls/buff_conn.go
Normal file
111
internal/outpost/radius/eap/protocol/tls/buff_conn.go
Normal file
@ -0,0 +1,111 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BuffConn struct {
|
||||
reader *bytes.Buffer
|
||||
writer *bytes.Buffer
|
||||
|
||||
ctx context.Context
|
||||
|
||||
expectedWriterByteCount int
|
||||
writtenByteCount int
|
||||
|
||||
retryOptions []retry.Option
|
||||
}
|
||||
|
||||
func NewBuffConn(initialData []byte, ctx context.Context) *BuffConn {
|
||||
c := &BuffConn{
|
||||
reader: bytes.NewBuffer(initialData),
|
||||
writer: bytes.NewBuffer([]byte{}),
|
||||
ctx: ctx,
|
||||
retryOptions: []retry.Option{
|
||||
retry.Context(ctx),
|
||||
retry.Delay(10 * time.Microsecond),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.MaxDelay(100 * time.Millisecond),
|
||||
retry.Attempts(0),
|
||||
},
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
var errStall = errors.New("Stall")
|
||||
|
||||
func (conn BuffConn) OutboundData() []byte {
|
||||
d, _ := retry.DoWithData(
|
||||
func() ([]byte, error) {
|
||||
b := conn.writer.Bytes()
|
||||
if len(b) < 1 {
|
||||
return nil, errStall
|
||||
}
|
||||
return b, nil
|
||||
},
|
||||
conn.retryOptions...,
|
||||
)
|
||||
return d
|
||||
}
|
||||
|
||||
func (conn *BuffConn) UpdateData(data []byte) {
|
||||
conn.reader.Write(data)
|
||||
conn.writtenByteCount += len(data)
|
||||
log.Debugf("TLS(buffcon): Appending new data %d (total %d, expecting %d)", len(data), conn.writtenByteCount, conn.expectedWriterByteCount)
|
||||
}
|
||||
|
||||
func (conn BuffConn) NeedsMoreData() bool {
|
||||
if conn.expectedWriterByteCount > 0 {
|
||||
return conn.reader.Len() < int(conn.expectedWriterByteCount)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (conn *BuffConn) Read(p []byte) (int, error) {
|
||||
d, err := retry.DoWithData(
|
||||
func() (int, error) {
|
||||
if conn.reader.Len() == 0 {
|
||||
log.Debugf("TLS(buffcon): Attempted read %d from empty buffer, stalling...", len(p))
|
||||
return 0, errStall
|
||||
}
|
||||
if conn.expectedWriterByteCount > 0 {
|
||||
// If we're waiting for more data, we need to stall
|
||||
if conn.writtenByteCount < int(conn.expectedWriterByteCount) {
|
||||
log.Debugf("TLS(buffcon): Attempted read %d while waiting for bytes %d, stalling...", len(p), conn.expectedWriterByteCount-conn.reader.Len())
|
||||
return 0, errStall
|
||||
}
|
||||
// If we have all the data, reset how much we're expecting to still get
|
||||
if conn.writtenByteCount == int(conn.expectedWriterByteCount) {
|
||||
conn.expectedWriterByteCount = 0
|
||||
}
|
||||
}
|
||||
if conn.reader.Len() == 0 {
|
||||
conn.writtenByteCount = 0
|
||||
}
|
||||
n, err := conn.reader.Read(p)
|
||||
log.Debugf("TLS(buffcon): Read: %d into %d (total %d)", n, len(p), conn.reader.Len())
|
||||
return n, err
|
||||
},
|
||||
conn.retryOptions...,
|
||||
)
|
||||
return d, err
|
||||
}
|
||||
|
||||
func (conn BuffConn) Write(p []byte) (int, error) {
|
||||
log.Debugf("TLS(buffcon): Write: %d", len(p))
|
||||
return conn.writer.Write(p)
|
||||
}
|
||||
|
||||
func (conn BuffConn) Close() error { return nil }
|
||||
func (conn BuffConn) LocalAddr() net.Addr { return nil }
|
||||
func (conn BuffConn) RemoteAddr() net.Addr { return nil }
|
||||
func (conn BuffConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (conn BuffConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (conn BuffConn) SetWriteDeadline(t time.Time) error { return nil }
|
10
internal/outpost/radius/eap/protocol/tls/flags.go
Normal file
10
internal/outpost/radius/eap/protocol/tls/flags.go
Normal file
@ -0,0 +1,10 @@
|
||||
package tls
|
||||
|
||||
type Flag byte
|
||||
|
||||
const (
|
||||
FlagLengthIncluded Flag = 1 << 7
|
||||
FlagMoreFragments Flag = 1 << 6
|
||||
FlagTLSStart Flag = 1 << 5
|
||||
FlagNone Flag = 0
|
||||
)
|
39
internal/outpost/radius/eap/protocol/tls/inner.go
Normal file
39
internal/outpost/radius/eap/protocol/tls/inner.go
Normal file
@ -0,0 +1,39 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
func (p *Payload) innerHandler(ctx protocol.Context) {
|
||||
d := make([]byte, 1024)
|
||||
if !ctx.IsProtocolStart(p.Inner.Type()) {
|
||||
ctx.Log().Debug("TLS: Reading from TLS for inner protocol")
|
||||
n, err := p.st.TLS.Read(d)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: Failed to read from TLS connection")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
// Truncate data to the size we read
|
||||
d = d[:n]
|
||||
}
|
||||
err := p.Inner.Decode(d)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to decode inner protocol")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
pl := p.Inner.Handle(ctx.Inner(p.Inner, p.Inner.Type()))
|
||||
enc, err := pl.Encode()
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to encode inner protocol")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
_, err = p.st.TLS.Write(enc)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("TLS: failed to write to TLS")
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
}
|
279
internal/outpost/radius/eap/protocol/tls/payload.go
Normal file
279
internal/outpost/radius/eap/protocol/tls/payload.go
Normal file
@ -0,0 +1,279 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/vendors/microsoft"
|
||||
)
|
||||
|
||||
const maxChunkSize = 1000
|
||||
const staleConnectionTimeout = 10
|
||||
|
||||
const TypeTLS protocol.Type = 13
|
||||
|
||||
func Protocol() protocol.Payload {
|
||||
return &Payload{}
|
||||
}
|
||||
|
||||
type Payload struct {
|
||||
Flags Flag
|
||||
Length uint32
|
||||
Data []byte
|
||||
|
||||
st *State
|
||||
Inner protocol.Payload
|
||||
}
|
||||
|
||||
func (p *Payload) Type() protocol.Type {
|
||||
return TypeTLS
|
||||
}
|
||||
|
||||
func (p *Payload) HasInner() protocol.Payload {
|
||||
return p.Inner
|
||||
}
|
||||
|
||||
func (p *Payload) Offerable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Payload) Decode(raw []byte) error {
|
||||
p.Flags = Flag(raw[0])
|
||||
raw = raw[1:]
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
if len(raw) < 4 {
|
||||
return errors.New("invalid size")
|
||||
}
|
||||
p.Length = binary.BigEndian.Uint32(raw)
|
||||
p.Data = raw[4:]
|
||||
} else {
|
||||
p.Data = raw[0:]
|
||||
}
|
||||
log.WithField("raw", debug.FormatBytes(p.Data)).WithField("size", len(p.Data)).WithField("flags", p.Flags).Trace("TLS: decode raw")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) Encode() ([]byte, error) {
|
||||
l := 1
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
l += 4
|
||||
}
|
||||
buff := make([]byte, len(p.Data)+l)
|
||||
buff[0] = byte(p.Flags)
|
||||
if p.Flags&FlagLengthIncluded != 0 {
|
||||
buff[1] = byte(p.Length >> 24)
|
||||
buff[2] = byte(p.Length >> 16)
|
||||
buff[3] = byte(p.Length >> 8)
|
||||
buff[4] = byte(p.Length)
|
||||
}
|
||||
if len(p.Data) > 0 {
|
||||
copy(buff[5:], p.Data)
|
||||
}
|
||||
return buff, nil
|
||||
}
|
||||
|
||||
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||
defer func() {
|
||||
ctx.SetProtocolState(TypeTLS, p.st)
|
||||
}()
|
||||
if ctx.IsProtocolStart(TypeTLS) {
|
||||
p.st = NewState(ctx).(*State)
|
||||
return &Payload{
|
||||
Flags: FlagTLSStart,
|
||||
}
|
||||
}
|
||||
p.st = ctx.GetProtocolState(TypeTLS).(*State)
|
||||
|
||||
if p.st.TLS == nil {
|
||||
p.tlsInit(ctx)
|
||||
} else if len(p.Data) > 0 {
|
||||
ctx.Log().Debug("TLS: Updating buffer with new TLS data from packet")
|
||||
if p.Flags&FlagLengthIncluded != 0 && p.st.Conn.expectedWriterByteCount == 0 {
|
||||
ctx.Log().Debugf("TLS: Expecting %d total bytes, will buffer", p.Length)
|
||||
p.st.Conn.expectedWriterByteCount = int(p.Length)
|
||||
} else if p.Flags&FlagLengthIncluded != 0 {
|
||||
ctx.Log().Debug("TLS: No length included, not buffering")
|
||||
p.st.Conn.expectedWriterByteCount = 0
|
||||
}
|
||||
p.st.Conn.UpdateData(p.Data)
|
||||
if !p.st.Conn.NeedsMoreData() && !p.st.HandshakeDone {
|
||||
// Wait for outbound data to be available
|
||||
p.st.Conn.OutboundData()
|
||||
}
|
||||
}
|
||||
// If we need more data, send the client the go-ahead
|
||||
if p.st.Conn.NeedsMoreData() {
|
||||
return &Payload{
|
||||
Flags: FlagNone,
|
||||
Length: 0,
|
||||
Data: []byte{},
|
||||
}
|
||||
}
|
||||
if p.st.HasMore() {
|
||||
return p.sendNextChunk()
|
||||
}
|
||||
if p.st.Conn.writer.Len() == 0 && p.st.HandshakeDone {
|
||||
if p.Inner != nil {
|
||||
ctx.Log().Debug("TLS: Handshake is done, delegating to inner protocol")
|
||||
p.innerHandler(ctx)
|
||||
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||
}
|
||||
defer p.st.ContextCancel()
|
||||
// If we don't have a final status from the handshake finished function, stall for time
|
||||
pst, _ := retry.DoWithData(
|
||||
func() (protocol.Status, error) {
|
||||
if p.st.FinalStatus == protocol.StatusUnknown {
|
||||
return p.st.FinalStatus, errStall
|
||||
}
|
||||
return p.st.FinalStatus, nil
|
||||
},
|
||||
retry.Context(p.st.Context),
|
||||
retry.Delay(10*time.Microsecond),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.MaxDelay(100*time.Millisecond),
|
||||
retry.Attempts(0),
|
||||
)
|
||||
ctx.EndInnerProtocol(pst)
|
||||
return nil
|
||||
}
|
||||
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||
}
|
||||
|
||||
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||
if r.Code != radius.CodeAccessAccept {
|
||||
return nil
|
||||
}
|
||||
if p.st == nil || !p.st.HandshakeDone {
|
||||
return nil
|
||||
}
|
||||
log.Debug("TLS: Adding MPPE Keys")
|
||||
// TLS overrides other protocols' MPPE keys
|
||||
if len(microsoft.MSMPPERecvKey_Get(r, q)) > 0 {
|
||||
microsoft.MSMPPERecvKey_Del(r)
|
||||
}
|
||||
if len(microsoft.MSMPPESendKey_Get(r, q)) > 0 {
|
||||
microsoft.MSMPPESendKey_Del(r)
|
||||
}
|
||||
microsoft.MSMPPERecvKey_Set(r, p.st.MPPEKey[:32])
|
||||
microsoft.MSMPPESendKey_Set(r, p.st.MPPEKey[64:64+32])
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Payload) tlsInit(ctx protocol.Context) {
|
||||
ctx.Log().Debug("TLS: no TLS connection in state yet, starting connection")
|
||||
p.st.Context, p.st.ContextCancel = context.WithTimeout(context.Background(), staleConnectionTimeout*time.Second)
|
||||
p.st.Conn = NewBuffConn(p.Data, p.st.Context)
|
||||
cfg := ctx.ProtocolSettings().(TLSConfig).TLSConfig().Clone()
|
||||
|
||||
if klp, ok := os.LookupEnv("SSLKEYLOGFILE"); ok {
|
||||
kl, err := os.OpenFile(klp, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
cfg.KeyLogWriter = kl
|
||||
}
|
||||
|
||||
cfg.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||
ctx.Log().Debugf("TLS: ClientHello: %+v\n", chi)
|
||||
p.st.ClientHello = chi
|
||||
return nil, nil
|
||||
}
|
||||
p.st.TLS = tls.Server(p.st.Conn, cfg)
|
||||
p.st.TLS.SetDeadline(time.Now().Add(staleConnectionTimeout * time.Second))
|
||||
go func() {
|
||||
err := p.st.TLS.HandshakeContext(p.st.Context)
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Debug("TLS: Handshake error")
|
||||
p.st.FinalStatus = protocol.StatusError
|
||||
ctx.EndInnerProtocol(protocol.StatusError)
|
||||
return
|
||||
}
|
||||
ctx.Log().Debug("TLS: handshake done")
|
||||
p.tlsHandshakeFinished(ctx)
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *Payload) tlsHandshakeFinished(ctx protocol.Context) {
|
||||
cs := p.st.TLS.ConnectionState()
|
||||
label := "client EAP encryption"
|
||||
var context []byte
|
||||
switch cs.Version {
|
||||
case tls.VersionTLS10:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.0)", cs.Version)
|
||||
case tls.VersionTLS11:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.1)", cs.Version)
|
||||
case tls.VersionTLS12:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.2)", cs.Version)
|
||||
case tls.VersionTLS13:
|
||||
ctx.Log().Debugf("TLS: Version %d (1.3)", cs.Version)
|
||||
label = "EXPORTER_EAP_TLS_Key_Material"
|
||||
context = []byte{byte(TypeTLS)}
|
||||
}
|
||||
ksm, err := cs.ExportKeyingMaterial(label, context, 64+64)
|
||||
ctx.Log().Debugf("TLS: ksm % x %v", ksm, err)
|
||||
p.st.MPPEKey = ksm
|
||||
p.st.HandshakeDone = true
|
||||
if p.Inner == nil {
|
||||
p.st.FinalStatus = ctx.ProtocolSettings().(Settings).HandshakeSuccessful(ctx, cs.PeerCertificates)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) startChunkedTransfer(data []byte) *Payload {
|
||||
if len(data) > maxChunkSize {
|
||||
log.WithField("length", len(data)).Debug("TLS: Data needs to be chunked")
|
||||
p.st.RemainingChunks = append(p.st.RemainingChunks, slices.Collect(slices.Chunk(data, maxChunkSize))...)
|
||||
p.st.TotalPayloadSize = len(data)
|
||||
return p.sendNextChunk()
|
||||
}
|
||||
log.WithField("length", len(data)).Debug("TLS: Sending data un-chunked")
|
||||
p.st.Conn.writer.Reset()
|
||||
return &Payload{
|
||||
Flags: FlagLengthIncluded,
|
||||
Length: uint32(len(data)),
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) sendNextChunk() *Payload {
|
||||
nextChunk := p.st.RemainingChunks[0]
|
||||
log.WithField("raw", debug.FormatBytes(nextChunk)).Debug("TLS: Sending next chunk")
|
||||
p.st.RemainingChunks = p.st.RemainingChunks[1:]
|
||||
flags := FlagLengthIncluded
|
||||
if p.st.HasMore() {
|
||||
log.WithField("chunks", len(p.st.RemainingChunks)).Debug("TLS: More chunks left")
|
||||
flags += FlagMoreFragments
|
||||
} else {
|
||||
// Last chunk, reset the connection buffers and pending payload size
|
||||
defer func() {
|
||||
log.Debug("TLS: Sent last chunk")
|
||||
p.st.Conn.writer.Reset()
|
||||
p.st.TotalPayloadSize = 0
|
||||
}()
|
||||
}
|
||||
log.WithField("length", p.st.TotalPayloadSize).Debug("TLS: Total payload size")
|
||||
return &Payload{
|
||||
Flags: flags,
|
||||
Length: uint32(p.st.TotalPayloadSize),
|
||||
Data: nextChunk,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Payload) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<TLS Packet HandshakeDone=%t, FinalStatus=%d, ClientHello=%v>",
|
||||
p.st.HandshakeDone,
|
||||
p.st.FinalStatus,
|
||||
p.st.ClientHello,
|
||||
)
|
||||
}
|
21
internal/outpost/radius/eap/protocol/tls/settings.go
Normal file
21
internal/outpost/radius/eap/protocol/tls/settings.go
Normal file
@ -0,0 +1,21 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type TLSConfig interface {
|
||||
TLSConfig() *tls.Config
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Config *tls.Config
|
||||
HandshakeSuccessful func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status
|
||||
}
|
||||
|
||||
func (s Settings) TLSConfig() *tls.Config {
|
||||
return s.Config
|
||||
}
|
32
internal/outpost/radius/eap/protocol/tls/state.go
Normal file
32
internal/outpost/radius/eap/protocol/tls/state.go
Normal file
@ -0,0 +1,32 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
RemainingChunks [][]byte
|
||||
HandshakeDone bool
|
||||
FinalStatus protocol.Status
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
MPPEKey []byte
|
||||
TotalPayloadSize int
|
||||
TLS *tls.Conn
|
||||
Conn *BuffConn
|
||||
Context context.Context
|
||||
ContextCancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewState(c protocol.Context) interface{} {
|
||||
c.Log().Debug("TLS: new state")
|
||||
return &State{
|
||||
RemainingChunks: make([][]byte, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (s State) HasMore() bool {
|
||||
return len(s.RemainingChunks) > 0
|
||||
}
|
@ -1,17 +1,44 @@
|
||||
package radius
|
||||
|
||||
import (
|
||||
"context"
|
||||
ttls "crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api/v3"
|
||||
"goauthentik.io/internal/outpost/flow"
|
||||
"goauthentik.io/internal/outpost/radius/eap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/gtc"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/mschapv2"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
"goauthentik.io/internal/utils"
|
||||
"layeh.com/radius"
|
||||
"layeh.com/radius/rfc2865"
|
||||
"layeh.com/radius/rfc2869"
|
||||
)
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
eap := rfc2869.EAPMessage_Get(r.Packet)
|
||||
if len(eap) > 0 {
|
||||
rs.log.Trace("EAP request")
|
||||
rs.Handle_AccessRequest_EAP(w, r)
|
||||
} else {
|
||||
rs.log.Trace("PAP request")
|
||||
rs.Handle_AccessRequest_PAP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_PAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
username := rfc2865.UserName_GetString(r.Packet)
|
||||
|
||||
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
@ -87,3 +114,164 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
|
||||
res.Add(attr.Type, attr.Attribute)
|
||||
}
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) Handle_AccessRequest_EAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||
er := rfc2869.EAPMessage_Get(r.Packet)
|
||||
ep, err := eap.Decode(r.pi, er)
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("failed to parse EAP packet")
|
||||
return
|
||||
}
|
||||
ep.HandleRadiusPacket(w, r.Request)
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetEAPState(key string) *protocol.State {
|
||||
return pi.eapState[key]
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) SetEAPState(key string, state *protocol.State) {
|
||||
pi.eapState[key] = state
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) GetEAPSettings() protocol.Settings {
|
||||
protocols := []protocol.ProtocolConstructor{
|
||||
identity.Protocol,
|
||||
legacy_nak.Protocol,
|
||||
}
|
||||
|
||||
certId := pi.certId
|
||||
if certId == "" {
|
||||
return protocol.Settings{
|
||||
Protocols: protocols,
|
||||
}
|
||||
}
|
||||
|
||||
cert := pi.s.cryptoStore.Get(certId)
|
||||
if cert == nil {
|
||||
return protocol.Settings{
|
||||
Protocols: protocols,
|
||||
}
|
||||
}
|
||||
|
||||
return protocol.Settings{
|
||||
Protocols: append(protocols, tls.Protocol, peap.Protocol),
|
||||
ProtocolPriority: []protocol.Type{tls.TypeTLS, peap.TypePEAP},
|
||||
ProtocolSettings: map[protocol.Type]interface{}{
|
||||
tls.TypeTLS: tls.Settings{
|
||||
Config: &ttls.Config{
|
||||
Certificates: []ttls.Certificate{*cert},
|
||||
ClientAuth: ttls.RequireAnyClientCert,
|
||||
},
|
||||
HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status {
|
||||
ctx.Log().Debug("Starting authn flow")
|
||||
pem := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "CERTIFICATE",
|
||||
Bytes: certs[0].Raw,
|
||||
})
|
||||
|
||||
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||
})
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
fe.AddHeader("X-Authentik-Outpost-Certificate", url.QueryEscape(string(pem)))
|
||||
|
||||
passed, err := fe.Execute()
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||
return protocol.StatusError
|
||||
}
|
||||
ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||
if passed {
|
||||
return protocol.StatusSuccess
|
||||
} else {
|
||||
return protocol.StatusError
|
||||
}
|
||||
},
|
||||
},
|
||||
peap.TypePEAP: peap.Settings{
|
||||
Config: &ttls.Config{
|
||||
Certificates: []ttls.Certificate{*cert},
|
||||
},
|
||||
InnerProtocols: protocol.Settings{
|
||||
Protocols: append(protocols, gtc.Protocol, mschapv2.Protocol),
|
||||
ProtocolPriority: []protocol.Type{gtc.TypeGTC, mschapv2.TypeMSCHAPv2},
|
||||
ProtocolSettings: map[protocol.Type]interface{}{
|
||||
mschapv2.TypeMSCHAPv2: mschapv2.Settings{
|
||||
AuthenticateRequest: mschapv2.DebugStaticCredentials(
|
||||
[]byte("foo"), []byte("bar"),
|
||||
),
|
||||
},
|
||||
gtc.TypeGTC: gtc.Settings{
|
||||
ChallengeHandler: func(ctx protocol.Context) (gtc.GetChallenge, gtc.ValidateResponse) {
|
||||
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||
})
|
||||
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||
var ch []byte = nil
|
||||
var ans []byte = nil
|
||||
fe.InteractiveSolver = func(ct *api.ChallengeTypes, afesr api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
||||
comp := ct.GetActualInstance().(flow.ChallengeCommon).GetComponent()
|
||||
ch = []byte(comp)
|
||||
for {
|
||||
if ans == nil {
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
switch comp {
|
||||
case string(flow.StageIdentification):
|
||||
r := api.NewIdentificationChallengeResponseRequest(string(ans))
|
||||
return api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||
case string(flow.StagePassword):
|
||||
r := api.NewPasswordChallengeResponseRequest(string(ans))
|
||||
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||
}
|
||||
panic(comp)
|
||||
}
|
||||
passed := false
|
||||
done := false
|
||||
go func() {
|
||||
var err error
|
||||
passed, err = fe.Execute()
|
||||
done = true
|
||||
if err != nil {
|
||||
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||
// return protocol.StatusError
|
||||
}
|
||||
// ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||
// if passed {
|
||||
// return protocol.StatusSuccess
|
||||
// } else {
|
||||
// return protocol.StatusError
|
||||
// }
|
||||
}()
|
||||
return func() []byte {
|
||||
if done {
|
||||
status := protocol.StatusError
|
||||
if passed {
|
||||
status = protocol.StatusSuccess
|
||||
}
|
||||
ctx.EndInnerProtocol(status)
|
||||
}
|
||||
for {
|
||||
if ch == nil {
|
||||
continue
|
||||
}
|
||||
defer func() {
|
||||
ch = nil
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
}, func(answer []byte) {
|
||||
ans = answer
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package radius
|
||||
import (
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
@ -35,12 +36,32 @@ func (r *RadiusRequest) ID() string {
|
||||
return r.id
|
||||
}
|
||||
|
||||
type LogWriter struct {
|
||||
w radius.ResponseWriter
|
||||
l *log.Entry
|
||||
}
|
||||
|
||||
func (lw LogWriter) Write(packet *radius.Packet) error {
|
||||
lw.l.WithField("code", packet.Code.String()).Info("Radius Response")
|
||||
return lw.w.Write(packet)
|
||||
}
|
||||
|
||||
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
|
||||
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
|
||||
sentry.WithTransactionName("authentik.providers.radius.connect"))
|
||||
rid := uuid.New().String()
|
||||
span.SetTag("request_uid", rid)
|
||||
rl := rs.log.WithField("code", r.Code.String()).WithField("request", rid)
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr.String())
|
||||
if err != nil {
|
||||
rs.log.WithError(err).Warning("Failed to get remote IP")
|
||||
return
|
||||
}
|
||||
rl := rs.log.WithFields(log.Fields{
|
||||
"code": r.Code.String(),
|
||||
"request": rid,
|
||||
"ip": host,
|
||||
"id": r.Identifier,
|
||||
})
|
||||
selectedApp := ""
|
||||
defer func() {
|
||||
span.Finish()
|
||||
@ -58,6 +79,7 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
}
|
||||
|
||||
rl.Info("Radius Request")
|
||||
ww := LogWriter{w, rl}
|
||||
|
||||
// Lookup provider by shared secret
|
||||
var pi *ProviderInstance
|
||||
@ -72,12 +94,12 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
||||
hs := sha512.Sum512([]byte(r.Secret))
|
||||
bs := hex.EncodeToString(hs[:])
|
||||
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
|
||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
||||
_ = ww.Write(r.Response(radius.CodeAccessReject))
|
||||
return
|
||||
}
|
||||
nr.pi = pi
|
||||
|
||||
if nr.Code == radius.CodeAccessRequest {
|
||||
rs.Handle_AccessRequest(w, nr)
|
||||
rs.Handle_AccessRequest(ww, nr)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/config"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||
"goauthentik.io/internal/outpost/radius/metrics"
|
||||
|
||||
"layeh.com/radius"
|
||||
@ -22,23 +23,27 @@ type ProviderInstance struct {
|
||||
appSlug string
|
||||
flowSlug string
|
||||
providerId int32
|
||||
certId string
|
||||
s *RadiusServer
|
||||
log *log.Entry
|
||||
eapState map[string]*protocol.State
|
||||
}
|
||||
|
||||
type RadiusServer struct {
|
||||
s radius.PacketServer
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
s radius.PacketServer
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
cryptoStore *ak.CryptoStore
|
||||
|
||||
providers []*ProviderInstance
|
||||
providers map[int32]*ProviderInstance
|
||||
}
|
||||
|
||||
func NewServer(ac *ak.APIController) ak.Outpost {
|
||||
rs := &RadiusServer{
|
||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||
ac: ac,
|
||||
providers: []*ProviderInstance{},
|
||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||
ac: ac,
|
||||
providers: map[int32]*ProviderInstance{},
|
||||
cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi),
|
||||
}
|
||||
rs.s = radius.PacketServer{
|
||||
Handler: rs,
|
||||
@ -85,7 +90,7 @@ func (rs *RadiusServer) RADIUSSecret(ctx context.Context, remoteAddr net.Addr) (
|
||||
return bi < bj
|
||||
})
|
||||
candidate := matchedPrefixes[0]
|
||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).Debug("Matched CIDR")
|
||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).WithField("instance", candidate.p.appSlug).Debug("Matched CIDR")
|
||||
return candidate.p.SharedSecret, nil
|
||||
}
|
||||
|
||||
@ -98,7 +103,8 @@ func (rs *RadiusServer) Start() error {
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
err := rs.StartRadiusServer()
|
||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||
err := rs.s.ListenAndServe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ Parameters:
|
||||
Description: authentik Docker image
|
||||
AuthentikVersion:
|
||||
Type: String
|
||||
Default: 2025.6.2
|
||||
Default: 2025.6.3
|
||||
Description: authentik Docker image tag
|
||||
AuthentikServerCPU:
|
||||
Type: Number
|
||||
|
Binary file not shown.
@ -11,18 +11,18 @@
|
||||
# Nicola Mersi, 2024
|
||||
# tmassimi, 2024
|
||||
# Marc Schmitt, 2024
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2024
|
||||
# Matteo Piccina <altermatte@gmail.com>, 2025
|
||||
# Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025
|
||||
# albanobattistella <albanobattistella@gmail.com>, 2025
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-05-28 11:25+0000\n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: Kowalski Dragon (kowalski7cc) <kowalski.7cc@gmail.com>, 2025\n"
|
||||
"Last-Translator: albanobattistella <albanobattistella@gmail.com>, 2025\n"
|
||||
"Language-Team: Italian (https://app.transifex.com/authentik/teams/119923/it/)\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
@ -116,7 +116,7 @@ msgstr "Certificato Web utilizzato dal server Web authentik Core."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Certificates used for client authentication."
|
||||
msgstr ""
|
||||
msgstr "Certificati utilizzati per l'autenticazione del client."
|
||||
|
||||
#: authentik/brands/models.py
|
||||
msgid "Brand"
|
||||
@ -130,10 +130,6 @@ msgstr "Brands"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "L'utente non ha accesso all'applicazione."
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "Descrizione extra non disponibile"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "Impossibile impostare il gruppo come padre di se stesso."
|
||||
@ -294,15 +290,15 @@ msgid ""
|
||||
msgstr ""
|
||||
"Collegamento a un utente con indirizzo email identico. Può avere "
|
||||
"implicazioni sulla sicurezza quando una fonte non convalida gli indirizzi "
|
||||
"e-mail."
|
||||
"email."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
"Use the user's email address, but deny enrollment when the email address "
|
||||
"already exists."
|
||||
msgstr ""
|
||||
"Usa l'indirizzo e-mail dell'utente, ma nega l'iscrizione quando l'indirizzo "
|
||||
"e-mail esiste già."
|
||||
"Usa l'indirizzo email dell'utente, ma nega l'iscrizione quando l'indirizzo "
|
||||
"email esiste già."
|
||||
|
||||
#: authentik/core/models.py
|
||||
msgid ""
|
||||
@ -682,26 +678,29 @@ msgid ""
|
||||
"option has a higher priority than the `client_certificate` option on "
|
||||
"`Brand`."
|
||||
msgstr ""
|
||||
"Configura le autorità di certificazione per convalidare il certificato. "
|
||||
"Questa opzione ha una priorità maggiore rispetto all'opzione "
|
||||
"`client_certificate` su `Brand`."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Mutual TLS Stage"
|
||||
msgstr ""
|
||||
msgstr "Fase di TLS reciproca"
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Mutual TLS Stages"
|
||||
msgstr ""
|
||||
msgstr "Fasi di TLS reciproche"
|
||||
|
||||
#: authentik/enterprise/stages/mtls/models.py
|
||||
msgid "Permissions to pass Certificates for outposts."
|
||||
msgstr ""
|
||||
msgstr " Permessi di trasmissione dei Certificati per gli avamposti."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/stage.py
|
||||
msgid "Certificate required but no certificate was given."
|
||||
msgstr ""
|
||||
msgstr " Il certificato è stato richiesto ma non è stato consegnato."
|
||||
|
||||
#: authentik/enterprise/stages/mtls/stage.py
|
||||
msgid "No user found for certificate."
|
||||
msgstr ""
|
||||
msgstr "Nessun utente trovato per il certificato."
|
||||
|
||||
#: authentik/enterprise/stages/source/models.py
|
||||
msgid ""
|
||||
@ -834,6 +833,14 @@ msgstr ""
|
||||
"Definisci a quale gruppo di utenti deve essere inviata e mostrata questa "
|
||||
"notifica. Se lasciato vuoto, la notifica non verrà inviata."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr ""
|
||||
"Se abilitata, la notifica verrà inviata all'utente che ha attivato l'evento."
|
||||
" Se destination_group è configurato, la notifica verrà inviata a entrambi."
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "Regola di notifica"
|
||||
@ -1050,16 +1057,16 @@ msgstr "Avvio della sincronizzazione completa del provider"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing users"
|
||||
msgstr ""
|
||||
msgstr "Sincronizzazione degli utenti"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Syncing groups"
|
||||
msgstr ""
|
||||
msgstr "Sincronizzazione dei gruppi"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
#, python-brace-format
|
||||
msgid "Syncing page {page} of groups"
|
||||
msgstr "Sincronizzando pagina {page} dei gruppi"
|
||||
msgid "Syncing page {page} of {object_type}"
|
||||
msgstr "Sincronizzazione della pagina {page} di {object_type}"
|
||||
|
||||
#: authentik/lib/sync/outgoing/tasks.py
|
||||
msgid "Dropping mutating request due to dry run"
|
||||
@ -2461,6 +2468,10 @@ msgstr "Gruppo di aggiunta DN"
|
||||
msgid "Consider Objects matching this filter to be Users."
|
||||
msgstr "Considerare gli oggetti corrispondenti a questo filtro come Utenti."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Attribute which matches the value of `group_membership_field`."
|
||||
msgstr "Attributo che corrisponde al valore di `group_membership_field`."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "Field which contains members of a group."
|
||||
msgstr "Campo che contiene i membri di un gruppo."
|
||||
@ -2502,6 +2513,8 @@ msgid ""
|
||||
"Delete authentik users and groups which were previously supplied by this "
|
||||
"source, but are now missing from it."
|
||||
msgstr ""
|
||||
"Elimina gli utenti e i gruppi authentik precedentemente forniti da questa "
|
||||
"fonte, ma che ora mancano."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "LDAP Source"
|
||||
@ -2523,6 +2536,8 @@ msgstr "Mappature delle proprietà della sorgente LDAP"
|
||||
msgid ""
|
||||
"Unique ID used while checking if this object still exists in the directory."
|
||||
msgstr ""
|
||||
"ID univoco utilizzato per verificare se questo oggetto esiste ancora nella "
|
||||
"directory."
|
||||
|
||||
#: authentik/sources/ldap/models.py
|
||||
msgid "User LDAP Source Connection"
|
||||
@ -2920,7 +2935,7 @@ msgstr "Connessioni sorgente SAML di gruppo"
|
||||
#: authentik/sources/saml/views.py
|
||||
#, python-brace-format
|
||||
msgid "Continue to {source_name}"
|
||||
msgstr ""
|
||||
msgstr "Continua su {source_name}"
|
||||
|
||||
#: authentik/sources/scim/models.py
|
||||
msgid "SCIM Source"
|
||||
@ -2988,8 +3003,8 @@ msgstr "Fasi di configurazione dell'autenticatore email"
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Exception occurred while rendering E-mail template"
|
||||
msgstr ""
|
||||
"Eccezione verificatasi durante la visualizzazione del modello di posta "
|
||||
"elettronica"
|
||||
"Si è verificata un'eccezione durante la visualizzazione del modello di posta"
|
||||
" elettronica"
|
||||
|
||||
#: authentik/stages/authenticator_email/models.py
|
||||
msgid "Email Device"
|
||||
@ -3028,7 +3043,7 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Codice MFA via e-mail.\n"
|
||||
" Codice MFA via email.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.html
|
||||
@ -3054,7 +3069,7 @@ msgid ""
|
||||
"Email MFA code\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Codice e-mail MFA\n"
|
||||
"Codice email MFA\n"
|
||||
|
||||
#: authentik/stages/authenticator_email/templates/email/email_otp.txt
|
||||
#, python-format
|
||||
@ -3321,7 +3336,7 @@ msgstr "Consensi utente"
|
||||
|
||||
#: authentik/stages/consent/stage.py
|
||||
msgid "Invalid consent token, re-showing prompt"
|
||||
msgstr ""
|
||||
msgstr "Token di consenso non valido, viene nuovamente visualizzato il prompt"
|
||||
|
||||
#: authentik/stages/deny/models.py
|
||||
msgid "Deny Stage"
|
||||
@ -3341,11 +3356,11 @@ msgstr "Fasi fittizie"
|
||||
|
||||
#: authentik/stages/email/flow.py
|
||||
msgid "Continue to confirm this email address."
|
||||
msgstr ""
|
||||
msgstr "Continua per confermare questo indirizzo email."
|
||||
|
||||
#: authentik/stages/email/flow.py
|
||||
msgid "Link was already used, please request a new link."
|
||||
msgstr ""
|
||||
msgstr "Il collegamento è già stato utilizzato. Richiedine uno nuovo."
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Password Reset"
|
||||
@ -3365,7 +3380,7 @@ msgstr "Fase email"
|
||||
|
||||
#: authentik/stages/email/models.py
|
||||
msgid "Email Stages"
|
||||
msgstr "Fasi Email"
|
||||
msgstr "Fasi email"
|
||||
|
||||
#: authentik/stages/email/stage.py
|
||||
msgid "Successfully verified Email."
|
||||
@ -3467,7 +3482,7 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n"
|
||||
" Se non hai richiesto una modifica della password, ignora questa email. Il link sopra è valido per %(expires)s.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/password_reset.txt
|
||||
@ -3485,11 +3500,11 @@ msgid ""
|
||||
"If you did not request a password change, please ignore this email. The link above is valid for %(expires)s.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Se non hai richiesto una modifica della password, ignora questa e-mail. Il link sopra è valido per %(expires)s.\n"
|
||||
"Se non hai richiesto una modifica della password, ignora questa email. Il link sopra è valido per %(expires)s.\n"
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.html
|
||||
msgid "authentik Test-Email"
|
||||
msgstr "e-mail di prova di authentik"
|
||||
msgstr "email di prova di authentik"
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.html
|
||||
msgid ""
|
||||
@ -3498,7 +3513,7 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Questa è un'e-mail di prova per informarti che hai configurato correttamente le e-mail di authentik.\n"
|
||||
" Questa è un'email di prova per informarti che hai configurato correttamente le email di authentik.\n"
|
||||
" "
|
||||
|
||||
#: authentik/stages/email/templates/email/setup.txt
|
||||
@ -3507,7 +3522,7 @@ msgid ""
|
||||
"This is a test email to inform you, that you've successfully configured authentik emails.\n"
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Questa è un'e-mail di prova per informarti che hai configurato correttamente le e-mail di authentik.\n"
|
||||
"Questa è un'email di prova per informarti che hai configurato correttamente le email di authentik.\n"
|
||||
|
||||
#: authentik/stages/identification/api.py
|
||||
msgid "When no user fields are selected, at least one source must be selected"
|
||||
@ -3710,7 +3725,7 @@ msgstr ""
|
||||
|
||||
#: authentik/stages/prompt/models.py
|
||||
msgid "Email: Text field with Email type."
|
||||
msgstr "E-mail: Campo di testo con il tipo di e-mail."
|
||||
msgstr "Email: Campo di testo con il tipo di email."
|
||||
|
||||
#: authentik/stages/prompt/models.py
|
||||
msgid ""
|
||||
@ -3865,10 +3880,6 @@ msgstr "Fasi di accesso utente"
|
||||
msgid "No Pending user to login."
|
||||
msgstr "Nessun utente in attesa di accesso."
|
||||
|
||||
#: authentik/stages/user_login/stage.py
|
||||
msgid "Successfully logged in!"
|
||||
msgstr "Accesso effettuato!"
|
||||
|
||||
#: authentik/stages/user_logout/models.py
|
||||
msgid "User Logout Stage"
|
||||
msgstr "Fase di disconnessione dell'utente"
|
||||
|
Binary file not shown.
@ -15,7 +15,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese Simplified (https://app.transifex.com/authentik/teams/119923/zh-Hans/)\n"
|
||||
@ -118,10 +118,6 @@ msgstr "品牌"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "用户没有访问此应用程序的权限。"
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "额外描述不可用"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "无法设置组自身为父级。"
|
||||
@ -775,6 +771,12 @@ msgid ""
|
||||
"If left empty, Notification won't ben sent."
|
||||
msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr "启用时,通知会被发送到触发事件的用户。当配置了 destination_group 时,通知也会同时发送到对应组。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "通知规则"
|
||||
|
Binary file not shown.
@ -14,7 +14,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-06-04 00:12+0000\n"
|
||||
"POT-Creation-Date: 2025-06-25 00:10+0000\n"
|
||||
"PO-Revision-Date: 2022-09-26 16:47+0000\n"
|
||||
"Last-Translator: deluxghost, 2025\n"
|
||||
"Language-Team: Chinese (China) (https://app.transifex.com/authentik/teams/119923/zh_CN/)\n"
|
||||
@ -117,10 +117,6 @@ msgstr "品牌"
|
||||
msgid "User does not have access to application."
|
||||
msgstr "用户没有访问此应用程序的权限。"
|
||||
|
||||
#: authentik/core/api/devices.py
|
||||
msgid "Extra description not available"
|
||||
msgstr "额外描述不可用"
|
||||
|
||||
#: authentik/core/api/groups.py
|
||||
msgid "Cannot set group as parent of itself."
|
||||
msgstr "无法设置组自身为父级。"
|
||||
@ -774,6 +770,12 @@ msgid ""
|
||||
"If left empty, Notification won't ben sent."
|
||||
msgstr "定义此通知应该发送到哪些用户组。如果留空,则不会发送通知。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid ""
|
||||
"When enabled, notification will be sent to user the user that triggered the "
|
||||
"event.When destination_group is configured, notification is sent to both."
|
||||
msgstr "启用时,通知会被发送到触发事件的用户。当配置了 destination_group 时,通知也会同时发送到对应组。"
|
||||
|
||||
#: authentik/events/models.py
|
||||
msgid "Notification Rule"
|
||||
msgstr "通知规则"
|
||||
|
4
notes.md
Normal file
4
notes.md
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
eapol_test -s foo -a 192.168.68.1 -c config
|
||||
|
||||
sudo tcpdump -i bridge100 port 1812 -w eap.pcap
|
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.6.2",
|
||||
"version": "2025.6.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.6.2",
|
||||
"version": "2025.6.3",
|
||||
"devDependencies": {
|
||||
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
|
||||
"prettier": "^3.3.3",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@goauthentik/authentik",
|
||||
"version": "2025.6.2",
|
||||
"version": "2025.6.3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "authentik"
|
||||
version = "2025.6.2"
|
||||
version = "2025.6.3"
|
||||
description = ""
|
||||
authors = [{ name = "authentik Team", email = "hello@goauthentik.io" }]
|
||||
requires-python = "==3.13.*"
|
||||
@ -43,7 +43,7 @@ dependencies = [
|
||||
"jwcrypto==1.5.6",
|
||||
"kubernetes==33.1.0",
|
||||
"ldap3==2.9.1",
|
||||
"lxml==5.4.0",
|
||||
"lxml==6.0.0",
|
||||
"msgraph-sdk==1.35.0",
|
||||
"opencontainers==0.0.14",
|
||||
"packaging==25.0",
|
||||
@ -57,7 +57,7 @@ dependencies = [
|
||||
"pyyaml==6.0.2",
|
||||
"requests-oauthlib==2.0.0",
|
||||
"scim2-filter-parser==0.7.0",
|
||||
"sentry-sdk==2.31.0",
|
||||
"sentry-sdk==2.32.0",
|
||||
"service-identity==24.2.0",
|
||||
"setproctitle==1.3.6",
|
||||
"structlog==25.4.0",
|
||||
@ -67,7 +67,7 @@ dependencies = [
|
||||
"ua-parser==1.0.1",
|
||||
"unidecode==1.4.0",
|
||||
"urllib3<3",
|
||||
"uvicorn[standard]==0.34.3",
|
||||
"uvicorn[standard]==0.35.0",
|
||||
"watchdog==6.0.0",
|
||||
"webauthn==2.6.0",
|
||||
"wsproto==1.2.0",
|
||||
|
18
schema.yml
18
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2025.6.2
|
||||
version: 2025.6.3
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -54849,6 +54849,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
PatchedRedirectStageRequest:
|
||||
type: object
|
||||
description: RedirectStage Serializer
|
||||
@ -57302,6 +57306,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- application_slug
|
||||
- auth_flow_slug
|
||||
@ -57388,6 +57396,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- assigned_application_name
|
||||
- assigned_application_slug
|
||||
@ -57512,6 +57524,10 @@ components:
|
||||
should only be enabled if all users that will bind to this provider have
|
||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||
if it contains a semicolon.
|
||||
certificate:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
required:
|
||||
- authorization_flow
|
||||
- invalidation_flow
|
||||
|
@ -7,7 +7,7 @@ services:
|
||||
network_mode: host
|
||||
restart: always
|
||||
mailpit:
|
||||
image: docker.io/axllent/mailpit:v1.26.2
|
||||
image: docker.io/axllent/mailpit:v1.27.0
|
||||
ports:
|
||||
- 1025:1025
|
||||
- 8025:8025
|
||||
|
53
uv.lock
generated
53
uv.lock
generated
@ -165,7 +165,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "authentik"
|
||||
version = "2025.6.2"
|
||||
version = "2025.6.3"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "argon2-cffi" },
|
||||
@ -305,7 +305,7 @@ requires-dist = [
|
||||
{ name = "jwcrypto", specifier = "==1.5.6" },
|
||||
{ name = "kubernetes", specifier = "==33.1.0" },
|
||||
{ name = "ldap3", specifier = "==2.9.1" },
|
||||
{ name = "lxml", specifier = "==5.4.0" },
|
||||
{ name = "lxml", specifier = "==6.0.0" },
|
||||
{ name = "msgraph-sdk", specifier = "==1.35.0" },
|
||||
{ name = "opencontainers", git = "https://github.com/vsoch/oci-python?rev=ceb4fcc090851717a3069d78e85ceb1e86c2740c" },
|
||||
{ name = "packaging", specifier = "==25.0" },
|
||||
@ -319,7 +319,7 @@ requires-dist = [
|
||||
{ name = "pyyaml", specifier = "==6.0.2" },
|
||||
{ name = "requests-oauthlib", specifier = "==2.0.0" },
|
||||
{ name = "scim2-filter-parser", specifier = "==0.7.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.31.0" },
|
||||
{ name = "sentry-sdk", specifier = "==2.32.0" },
|
||||
{ name = "service-identity", specifier = "==24.2.0" },
|
||||
{ name = "setproctitle", specifier = "==1.3.6" },
|
||||
{ name = "structlog", specifier = "==25.4.0" },
|
||||
@ -329,7 +329,7 @@ requires-dist = [
|
||||
{ name = "ua-parser", specifier = "==1.0.1" },
|
||||
{ name = "unidecode", specifier = "==1.4.0" },
|
||||
{ name = "urllib3", specifier = "<3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.34.3" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = "==0.35.0" },
|
||||
{ name = "watchdog", specifier = "==6.0.0" },
|
||||
{ name = "webauthn", specifier = "==2.6.0" },
|
||||
{ name = "wsproto", specifier = "==1.2.0" },
|
||||
@ -1824,27 +1824,22 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "lxml"
|
||||
version = "5.4.0"
|
||||
version = "6.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/87/cb/2ba1e9dd953415f58548506fa5549a7f373ae55e80c61c9041b7fd09a38a/lxml-5.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:773e27b62920199c6197130632c18fb7ead3257fce1ffb7d286912e56ddb79e0", size = 8110086, upload-time = "2025-04-23T01:46:52.218Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/3e/6602a4dca3ae344e8609914d6ab22e52ce42e3e1638c10967568c5c1450d/lxml-5.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ce9c671845de9699904b1e9df95acfe8dfc183f2310f163cdaa91a3535af95de", size = 4404613, upload-time = "2025-04-23T01:46:55.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/72/bf00988477d3bb452bef9436e45aeea82bb40cdfb4684b83c967c53909c7/lxml-5.4.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9454b8d8200ec99a224df8854786262b1bd6461f4280064c807303c642c05e76", size = 5012008, upload-time = "2025-04-23T01:46:57.817Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/1f/93e42d93e9e7a44b2d3354c462cd784dbaaf350f7976b5d7c3f85d68d1b1/lxml-5.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cccd007d5c95279e529c146d095f1d39ac05139de26c098166c4beb9374b0f4d", size = 4760915, upload-time = "2025-04-23T01:47:00.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/0b/363009390d0b461cf9976a499e83b68f792e4c32ecef092f3f9ef9c4ba54/lxml-5.4.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0fce1294a0497edb034cb416ad3e77ecc89b313cff7adbee5334e4dc0d11f422", size = 5283890, upload-time = "2025-04-23T01:47:04.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/dc/6056c332f9378ab476c88e301e6549a0454dbee8f0ae16847414f0eccb74/lxml-5.4.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24974f774f3a78ac12b95e3a20ef0931795ff04dbb16db81a90c37f589819551", size = 4812644, upload-time = "2025-04-23T01:47:07.833Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/8a/f8c66bbb23ecb9048a46a5ef9b495fd23f7543df642dabeebcb2eeb66592/lxml-5.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:497cab4d8254c2a90bf988f162ace2ddbfdd806fce3bda3f581b9d24c852e03c", size = 4921817, upload-time = "2025-04-23T01:47:10.317Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/57/2e537083c3f381f83d05d9b176f0d838a9e8961f7ed8ddce3f0217179ce3/lxml-5.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:e794f698ae4c5084414efea0f5cc9f4ac562ec02d66e1484ff822ef97c2cadff", size = 4753916, upload-time = "2025-04-23T01:47:12.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/80/ea8c4072109a350848f1157ce83ccd9439601274035cd045ac31f47f3417/lxml-5.4.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:2c62891b1ea3094bb12097822b3d44b93fc6c325f2043c4d2736a8ff09e65f60", size = 5289274, upload-time = "2025-04-23T01:47:15.916Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/47/c4be287c48cdc304483457878a3f22999098b9a95f455e3c4bda7ec7fc72/lxml-5.4.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:142accb3e4d1edae4b392bd165a9abdee8a3c432a2cca193df995bc3886249c8", size = 4874757, upload-time = "2025-04-23T01:47:19.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/04/6ef935dc74e729932e39478e44d8cfe6a83550552eaa072b7c05f6f22488/lxml-5.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1a42b3a19346e5601d1b8296ff6ef3d76038058f311902edd574461e9c036982", size = 4947028, upload-time = "2025-04-23T01:47:22.401Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/f9/c33fc8daa373ef8a7daddb53175289024512b6619bc9de36d77dca3df44b/lxml-5.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4291d3c409a17febf817259cb37bc62cb7eb398bcc95c1356947e2871911ae61", size = 4834487, upload-time = "2025-04-23T01:47:25.513Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/30/fc92bb595bcb878311e01b418b57d13900f84c2b94f6eca9e5073ea756e6/lxml-5.4.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4f5322cf38fe0e21c2d73901abf68e6329dc02a4994e483adbcf92b568a09a54", size = 5381688, upload-time = "2025-04-23T01:47:28.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/d1/3ba7bd978ce28bba8e3da2c2e9d5ae3f8f521ad3f0ca6ea4788d086ba00d/lxml-5.4.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:0be91891bdb06ebe65122aa6bf3fc94489960cf7e03033c6f83a90863b23c58b", size = 5242043, upload-time = "2025-04-23T01:47:31.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/cd/95fa2201041a610c4d08ddaf31d43b98ecc4b1d74b1e7245b1abdab443cb/lxml-5.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:15a665ad90054a3d4f397bc40f73948d48e36e4c09f9bcffc7d90c87410e478a", size = 5021569, upload-time = "2025-04-23T01:47:33.805Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/a6/31da006fead660b9512d08d23d31e93ad3477dd47cc42e3285f143443176/lxml-5.4.0-cp313-cp313-win32.whl", hash = "sha256:d5663bc1b471c79f5c833cffbc9b87d7bf13f87e055a5c86c363ccd2348d7e82", size = 3485270, upload-time = "2025-04-23T01:47:36.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/14/c115516c62a7d2499781d2d3d7215218c0731b2c940753bf9f9b7b73924d/lxml-5.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:bcb7a1096b4b6b24ce1ac24d4942ad98f983cd3810f9711bcd0293f43a9d8b9f", size = 3814606, upload-time = "2025-04-23T01:47:39.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/79/21/6e7c060822a3c954ff085e5e1b94b4a25757c06529eac91e550f3f5cd8b8/lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da", size = 8414372, upload-time = "2025-06-26T16:26:39.079Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/f6/051b1607a459db670fc3a244fa4f06f101a8adf86cda263d1a56b3a4f9d5/lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7", size = 4593940, upload-time = "2025-06-26T16:26:41.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/74/dd595d92a40bda3c687d70d4487b2c7eff93fd63b568acd64fedd2ba00fe/lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3", size = 5214329, upload-time = "2025-06-26T16:26:44.669Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/4b/20555bdd75d57945bdabfbc45fdb1a36a1a0ff9eae4653e951b2b79c9209/lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24", size = 5021931, upload-time = "2025-06-26T16:26:47.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/dd/39c8507c16db6031f8c1ddf70ed95dbb0a6d466a40002a3522c128aba472/lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29", size = 5247467, upload-time = "2025-06-26T16:26:49.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4d/56/732d49def0631ad633844cfb2664563c830173a98d5efd9b172e89a4800d/lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4", size = 4720601, upload-time = "2025-06-26T16:26:52.564Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/7f/6b956fab95fa73462bca25d1ea7fc8274ddf68fb8e60b78d56c03b65278e/lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca", size = 5060227, upload-time = "2025-06-26T16:26:55.054Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/06/e851ac2924447e8b15a294855caf3d543424364a143c001014d22c8ca94c/lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf", size = 4790637, upload-time = "2025-06-26T16:26:57.384Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/52/03/0e764ce00b95e008d76b99d432f1807f3574fb2945b496a17807a1645dbd/lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef", size = 5272430, upload-time = "2025-06-26T16:27:00.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/01/d48cc141bc47bc1644d20fe97bbd5e8afb30415ec94f146f2f76d0d9d098/lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181", size = 3612896, upload-time = "2025-06-26T16:27:04.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/87/6456b9541d186ee7d4cb53bf1b9a0d7f3b1068532676940fdd594ac90865/lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e", size = 4013132, upload-time = "2025-06-26T16:27:06.415Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/42/85b3aa8f06ca0d24962f8100f001828e1f1f1a38c954c16e71154ed7d53a/lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03", size = 3672642, upload-time = "2025-06-26T16:27:09.888Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2961,15 +2956,15 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "2.31.0"
|
||||
version = "2.32.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d0/45/c7ef7e12d8434fda8b61cdab432d8af64fb832480c93cdaf4bdcab7f5597/sentry_sdk-2.31.0.tar.gz", hash = "sha256:fed6d847f15105849cdf5dfdc64dcec356f936d41abb8c9d66adae45e60959ec", size = 334167, upload-time = "2025-06-24T16:36:26.066Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/59/eb90c45cb836cf8bec973bba10230ddad1c55e2b2e9ffa9d7d7368948358/sentry_sdk-2.32.0.tar.gz", hash = "sha256:9016c75d9316b0f6921ac14c8cd4fb938f26002430ac5be9945ab280f78bec6b", size = 334932, upload-time = "2025-06-27T08:10:02.89Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/a2/9b6d8cc59f03251c583b3fec9d2f075dc09c0f6e030e0e0a3b223c6e64b2/sentry_sdk-2.31.0-py2.py3-none-any.whl", hash = "sha256:e953f5ab083e6599bab255b75d6829b33b3ddf9931a27ca00b4ab0081287e84f", size = 355638, upload-time = "2025-06-24T16:36:24.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/a1/fc4856bd02d2097324fb7ce05b3021fb850f864b83ca765f6e37e92ff8ca/sentry_sdk-2.32.0-py2.py3-none-any.whl", hash = "sha256:6cf51521b099562d7ce3606da928c473643abe99b00ce4cb5626ea735f4ec345", size = 356122, upload-time = "2025-06-27T08:10:01.424Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -3322,15 +3317,15 @@ socks = [
|
||||
|
||||
[[package]]
|
||||
name = "uvicorn"
|
||||
version = "0.34.3"
|
||||
version = "0.35.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "click" },
|
||||
{ name = "h11" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/de/ad/713be230bcda622eaa35c28f0d328c3675c371238470abdea52417f17a8e/uvicorn-0.34.3.tar.gz", hash = "sha256:35919a9a979d7a59334b6b10e05d77c1d0d574c50e0fc98b8b1a0f165708b55a", size = 76631, upload-time = "2025-06-01T07:48:17.531Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/0d/8adfeaa62945f90d19ddc461c55f4a50c258af7662d34b6a3d5d1f8646f6/uvicorn-0.34.3-py3-none-any.whl", hash = "sha256:16246631db62bdfbf069b0645177d6e8a77ba950cfedbfd093acef9444e4d885", size = 62431, upload-time = "2025-06-01T07:48:15.664Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
||||
@ -93,6 +94,14 @@ export function renderForm(
|
||||
help=${clientNetworksHelp}
|
||||
input-hint="code"
|
||||
></ak-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? undefined)}
|
||||
singleton
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Certificate used for EAP-TLS.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
|
@ -478,8 +478,10 @@ export abstract class Table<T> extends WithLicenseSummary(AKElement) implements
|
||||
renderSearch(): TemplateResult {
|
||||
const runSearch = (value: string) => {
|
||||
this.search = value;
|
||||
this.page = 1;
|
||||
updateURLParams({
|
||||
search: value,
|
||||
tablePage: 1,
|
||||
});
|
||||
this.fetch();
|
||||
};
|
||||
|
@ -3,7 +3,7 @@ import { updateURLParams } from "#elements/router/RouteMatch";
|
||||
import { Table } from "#elements/table/Table";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult } from "lit";
|
||||
import { CSSResult, nothing } from "lit";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
@ -45,7 +45,7 @@ export abstract class TablePage<T> extends Table<T> {
|
||||
: html`<ak-empty-state icon=${this.pageIcon()}
|
||||
><span>${msg("No objects found.")}</span>
|
||||
<div slot="body">
|
||||
${this.searchEnabled() ? this.renderEmptyClearSearch() : html``}
|
||||
${this.searchEnabled() ? this.renderEmptyClearSearch() : nothing}
|
||||
</div>
|
||||
<div slot="primary">${this.renderObjectCreate()}</div>
|
||||
</ak-empty-state>`}
|
||||
@ -61,8 +61,10 @@ export abstract class TablePage<T> extends Table<T> {
|
||||
this.search = "";
|
||||
this.requestUpdate();
|
||||
this.fetch();
|
||||
this.page = 1;
|
||||
updateURLParams({
|
||||
search: "",
|
||||
tablePage: 1,
|
||||
});
|
||||
}}
|
||||
class="pf-c-button pf-m-link"
|
||||
|
@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" ?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<?xml version="1.0"?><xliff xmlns="urn:oasis:names:tc:xliff:document:1.2" version="1.2">
|
||||
<file target-language="it" source-language="en" original="lit-localize-inputs" datatype="plaintext">
|
||||
<body>
|
||||
<trans-unit id="s4caed5b7a7e5d89b">
|
||||
@ -596,9 +596,9 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="saa0e2675da69651b">
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>La URL "
|
||||
<x id="0" equiv-text="${this.url}"/>" non è stata trovata.</target>
|
||||
<source>The URL "<x id="0" equiv-text="${this.url}"/>" was not found.</source>
|
||||
<target>La URL "
|
||||
<x id="0" equiv-text="${this.url}"/>" non è stata trovata.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s58cd9c2fe836d9c6">
|
||||
@ -1100,7 +1100,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="sde949d0ef44572eb">
|
||||
<source>Requires the user to have a 'upn' attribute set, and falls back to hashed user ID. Use this mode only if you have different UPN and Mail domains.</source>
|
||||
<target>Richiede che l'utente abbia un attributo "upn" impostato e ricorre all'ID utente con hash. Utilizza questa modalità solo se disponi di domini UPN e di posta diversi.</target>
|
||||
<target>Richiede che l'utente abbia un attributo "upn" impostato e ricorre all'ID utente con hash. Utilizza questa modalità solo se disponi di domini UPN e di posta diversi.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f23ed1799b4d49a">
|
||||
@ -1260,7 +1260,7 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="s211b75e868072162">
|
||||
<source>Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'.</source>
|
||||
<target>Impostalo sul dominio per il quale desideri che l'autenticazione sia valida. Deve essere un dominio principale dell'URL riportato sopra. Se esegui applicazioni come app1.domain.tld, app2.domain.tld, impostalo su "domain.tld".</target>
|
||||
<target>Impostalo sul dominio per il quale desideri che l'autenticazione sia valida. Deve essere un dominio principale dell'URL riportato sopra. Se esegui applicazioni come app1.domain.tld, app2.domain.tld, impostalo su "domain.tld".</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s2345170f7e272668">
|
||||
@ -1709,8 +1709,8 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa90b7809586c35ce">
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>Inserisci un URL completo, un percorso relativo oppure utilizza "fa://fa-test" per utilizzare l'icona "fa-test" di Font Awesome.</target>
|
||||
<source>Either input a full URL, a relative path, or use 'fa://fa-test' to use the Font Awesome icon "fa-test".</source>
|
||||
<target>Inserisci un URL completo, un percorso relativo oppure utilizza "fa://fa-test" per utilizzare l'icona "fa-test" di Font Awesome.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s0410779cb47de312">
|
||||
@ -3134,7 +3134,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
</trans-unit>
|
||||
<trans-unit id="s3198c384c2f68b08">
|
||||
<source>Time offset when temporary users should be deleted. This only applies if your IDP uses the NameID Format 'transient', and the user doesn't log out manually.</source>
|
||||
<target>Tempo da attendere quando gli utenti temporanei devono essere eliminati. Questo vale solo se l'IDP utilizza il formato NameID "Transient" e l'utente non si disconnette manualmente.</target>
|
||||
<target>Tempo da attendere quando gli utenti temporanei devono essere eliminati. Questo vale solo se l'IDP utilizza il formato NameID "Transient" e l'utente non si disconnette manualmente.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sb32e9c1faa0b8673">
|
||||
@ -3276,7 +3276,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
</trans-unit>
|
||||
<trans-unit id="s9f8aac89fe318acc">
|
||||
<source>Optionally set the 'FriendlyName' value of the Assertion attribute.</source>
|
||||
<target>Opzionale: imposta il valore "friendlyname" dell'attributo di asserzione.</target>
|
||||
<target>Opzionale: imposta il valore "friendlyname" dell'attributo di asserzione.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s851c108679653d2a">
|
||||
@ -3757,10 +3757,10 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa95a538bfbb86111">
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<source>Are you sure you want to update <x id="0" equiv-text="${this.objectLabel}"/> "<x id="1" equiv-text="${this.obj?.name}"/>"?</source>
|
||||
<target>Sei sicuro di voler aggiornare
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>"?</target>
|
||||
<x id="0" equiv-text="${this.objectLabel}"/>"
|
||||
<x id="1" equiv-text="${this.obj?.name}"/>"?</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc92d7cfb6ee1fec6">
|
||||
@ -4133,7 +4133,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
</trans-unit>
|
||||
<trans-unit id="s7520286c8419a266">
|
||||
<source>Optional data which is loaded into the flow's 'prompt_data' context variable. YAML or JSON.</source>
|
||||
<target>Dati facoltativi che vengono caricati nella variabile di contesto "prompt_data" del flusso. YAML o JSON.</target>
|
||||
<target>Dati facoltativi che vengono caricati nella variabile di contesto "prompt_data" del flusso. YAML o JSON.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sb8795b799c70776a">
|
||||
@ -4826,8 +4826,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sdf1d8edef27236f0">
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<target>Un autenticatore "roaming", come un YubiKey</target>
|
||||
<source>A "roaming" authenticator, like a YubiKey</source>
|
||||
<target>Un autenticatore "roaming", come un YubiKey</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sfffba7b23d8fb40c">
|
||||
@ -5132,7 +5132,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
</trans-unit>
|
||||
<trans-unit id="s5170f9ef331949c0">
|
||||
<source>Show arbitrary input fields to the user, for example during enrollment. Data is saved in the flow context under the 'prompt_data' variable.</source>
|
||||
<target>Mostra campi di input arbitrari all'utente, ad esempio durante l'iscrizione. I dati vengono salvati nel contesto di flusso nell'ambito della variabile "prompt_data".</target>
|
||||
<target>Mostra campi di input arbitrari all'utente, ad esempio durante l'iscrizione. I dati vengono salvati nel contesto di flusso nell'ambito della variabile "prompt_data".</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s36cb242ac90353bc">
|
||||
@ -5185,8 +5185,8 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s1608b2f94fa0dbd4">
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<target>Se impostato su una durata superiore a 0, l'utente avrà la possibilità di scegliere di "rimanere firmato", che estenderà la sessione entro il momento specificato qui.</target>
|
||||
<source>If set to a duration above 0, the user will have the option to choose to "stay signed in", which will extend their session by the time specified here.</source>
|
||||
<target>Se impostato su una durata superiore a 0, l'utente avrà la possibilità di scegliere di "rimanere firmato", che estenderà la sessione entro il momento specificato qui.</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s542a71bb8f41e057">
|
||||
@ -5207,7 +5207,7 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<trans-unit id="s927398c400970760">
|
||||
<source>Write any data from the flow's context's 'prompt_data' to the currently pending user. If no user
|
||||
is pending, a new user is created, and data is written to them.</source>
|
||||
<target>Scrivi qualsiasi dati dal contesto del flusso "prompt_data" all'utente attualmente in sospeso. Se nessun utente
|
||||
<target>Scrivi qualsiasi dati dal contesto del flusso "prompt_data" all'utente attualmente in sospeso. Se nessun utente
|
||||
è in sospeso, viene creato un nuovo utente e vengono scritti dati.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sb379d861cbed0b47">
|
||||
@ -7336,7 +7336,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s070fdfb03034ca9b">
|
||||
<source>One hint, 'New Application Wizard', is currently hidden</source>
|
||||
<target>Un suggerimento, "New Application Wizard", è attualmente nascosto</target>
|
||||
<target>Un suggerimento, "New Application Wizard", è attualmente nascosto</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s1cc306d8e28c4464">
|
||||
<source>Deny message</source>
|
||||
@ -7451,7 +7451,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>Utente creato con successo e aggiunto al gruppo <x id="0" equiv-text="${this.group.name}"/></target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s824e0943a7104668">
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<source>This user will be added to the group "<x id="0" equiv-text="${this.targetGroup.name}"/>".</source>
|
||||
<target>Questo utente sarà aggiunto al gruppo &quot;<x id="0" equiv-text="${this.targetGroup.name}"/>&quot;.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s62e7f6ed7d9cb3ca">
|
||||
@ -8598,7 +8598,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s9dda0789d278f5c5">
|
||||
<source>Provide users with a 'show password' button.</source>
|
||||
<target>Fornisci agli utenti un pulsante "Mostra password".</target>
|
||||
<target>Fornisci agli utenti un pulsante "Mostra password".</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2f7f35f6a5b733f5">
|
||||
<source>Show password</source>
|
||||
@ -8733,7 +8733,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>Gruppo di sincronizzazione</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2d5f69929bb7221d">
|
||||
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
|
||||
<source><x id="0" equiv-text="${p.name}"/> ("<x id="1" equiv-text="${p.fieldKey}"/>", of type <x id="2" equiv-text="${p.type}"/>)</source>
|
||||
<target><x id="0" equiv-text="${p.name}"/> (&quot;<x id="1" equiv-text="${p.fieldKey}"/>&quot;, del tipo <x id="2" equiv-text="${p.type}"/>)</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s25bacc19d98b444e">
|
||||
@ -8981,8 +8981,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>URI di reindirizzamento validi dopo un flusso di autorizzazione riuscito. Specificare anche eventuali origini per i flussi impliciti.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s4c49d27de60a532b">
|
||||
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>Per consentire qualsiasi URI di reindirizzamento, imposta la modalità su Regex e il valore su ".*". Tieni presente le possibili implicazioni per la sicurezza.</target>
|
||||
<source>To allow any redirect URI, set the mode to Regex and the value to ".*". Be aware of the possible security implications this can have.</source>
|
||||
<target>Per consentire qualsiasi URI di reindirizzamento, imposta la modalità su Regex e il valore su ".*". Tieni presente le possibili implicazioni per la sicurezza.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sa52bf79fe1ccb13e">
|
||||
<source>Federated OIDC Sources</source>
|
||||
@ -9648,7 +9648,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s17359123e1f24504">
|
||||
<source>Field which contains DNs of groups the user is a member of. This field is used to lookup groups from users, e.g. 'memberOf'. To lookup nested groups in an Active Directory environment use 'memberOf:1.2.840.113556.1.4.1941:'.</source>
|
||||
<target>Campo che contiene i ND dei gruppi di cui l'utente è membro. Questo campo viene utilizzato per cercare i gruppi degli utenti, ad esempio "memberOf". Per cercare gruppi nidificati in un ambiente Active Directory, utilizzare "memberOf:1.2.840.113556.1.4.1941:".</target>
|
||||
<target>Campo che contiene i ND dei gruppi di cui l'utente è membro. Questo campo viene utilizzato per cercare i gruppi degli utenti, ad esempio "memberOf". Per cercare gruppi nidificati in un ambiente Active Directory, utilizzare "memberOf:1.2.840.113556.1.4.1941:".</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s891cd64acabf23bf">
|
||||
<source>Initial Permissions</source>
|
||||
@ -9731,8 +9731,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<target>Come eseguire l'autenticazione durante un flusso di richiesta del token authorization_code</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s844baf19a6c4a9b4">
|
||||
<source>Enable "Remember me on this device"</source>
|
||||
<target>Abilita "Ricordami su questo dispositivo"</target>
|
||||
<source>Enable "Remember me on this device"</source>
|
||||
<target>Abilita "Ricordami su questo dispositivo"</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sfa72bca733f40692">
|
||||
<source>When enabled, the user can save their username in a cookie, allowing them to skip directly to entering their password.</source>
|
||||
@ -9884,7 +9884,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="se630f2ccd39bf9e6">
|
||||
<source>If no group is selected and 'Send notification to event user' is disabled the rule is disabled. </source>
|
||||
<target>Se non viene selezionato alcun gruppo e l'opzione "Invia notifica all'utente dell'evento" è disabilitata, la regola è disabilitata.</target>
|
||||
<target>Se non viene selezionato alcun gruppo e l'opzione "Invia notifica all'utente dell'evento" è disabilitata, la regola è disabilitata.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47966b2a708694e2">
|
||||
<source>Send notification to event user</source>
|
||||
@ -9892,7 +9892,7 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd30f00ff2135589c">
|
||||
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
|
||||
<target>Se abilitata, la notifica verrà inviata all'utente che ha attivato l'evento, oltre a tutti gli utenti del gruppo sopra indicato. L'utente dell'evento sarà sempre il primo a inviare una notifica solo all'utente dell'evento che ha abilitato "Invia una volta" nel trasporto delle notifiche.</target>
|
||||
<target>Se abilitata, la notifica verrà inviata all'utente che ha attivato l'evento, oltre a tutti gli utenti del gruppo sopra indicato. L'utente dell'evento sarà sempre il primo a inviare una notifica solo all'utente dell'evento che ha abilitato "Invia una volta" nel trasporto delle notifiche.</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbd65aeeb8a3b9bbc">
|
||||
<source>Maximum registration attempts</source>
|
||||
@ -9908,7 +9908,8 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7225aacf0eee94d2">
|
||||
<source>Authenticated as <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/></source>
|
||||
<target>Autenticato come <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/></target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
</xliff>
|
||||
</xliff>
|
||||
|
@ -9876,30 +9876,39 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0433d667ea6eec1a">
|
||||
<source>The name of an invitation must be a slug: only lower case letters, numbers, and the hyphen are permitted here.</source>
|
||||
<target>邀请名称必须是一个 Slug:仅允许小写字母、数字和连字符。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e9d5ea88f02ae68">
|
||||
<source>Select the group of users which the alerts are sent to. </source>
|
||||
<target>选择一组用于发送警告的用户。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se630f2ccd39bf9e6">
|
||||
<source>If no group is selected and 'Send notification to event user' is disabled the rule is disabled. </source>
|
||||
<target>如果未选择组,并且“发送通知给事件用户”被禁用,则此规则被禁用。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47966b2a708694e2">
|
||||
<source>Send notification to event user</source>
|
||||
<target>发送通知给事件用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd30f00ff2135589c">
|
||||
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
|
||||
<target>启用时,通知不仅会发送给触发事件的用户,还会发送到组中的任何用户。事件用户将总是第一个用户,要只向事件用户发送通知,则需要在通知传输中启用“发送一次”。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbd65aeeb8a3b9bbc">
|
||||
<source>Maximum registration attempts</source>
|
||||
<target>最大注册尝试次数</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8495753cb15e8d8e">
|
||||
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
|
||||
<target>允许的最大注册尝试次数。设置为 0 则不限制次数。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sab4db6a3bd6abc1e">
|
||||
<source>This application does currently not have any application entitlements defined.</source>
|
||||
<target>此应用程序目前没有定义任何应用程序授权。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7225aacf0eee94d2">
|
||||
<source>Authenticated as <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/></source>
|
||||
<target>以 <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/> 身份通过验证</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -451,7 +451,7 @@
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sc35581d9c1cd67ff">
|
||||
<source>On behalf of <x id="0" equiv-text="${event.user.on_behalf_of.username}"/></source>
|
||||
<source>On behalf of <x id="0" equiv-text="${renderUsername(event.user.on_behalf_of)}"/></source>
|
||||
<target>代表
|
||||
<x id="0" equiv-text="${event.user.on_behalf_of.username}"/></target>
|
||||
|
||||
@ -5702,11 +5702,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Successfully created rule.</source>
|
||||
<target>已成功创建规则。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sa55ee64c5c51df0f">
|
||||
<source>Select the group of users which the alerts are sent to. If no group is selected the rule is disabled.</source>
|
||||
<target>选择一组用于发送警告的用户。如果未选择组,则此规则被禁用。</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sffa171e11d4ae513">
|
||||
<source>Transports</source>
|
||||
@ -5742,11 +5737,6 @@ doesn't pass when either or both of the selected options are equal or above the
|
||||
<source>Notification rule(s)</source>
|
||||
<target>通知规则</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="s5140d157642d7362">
|
||||
<source>None (rule disabled)</source>
|
||||
<target>无(规则已禁用)</target>
|
||||
|
||||
</trans-unit>
|
||||
<trans-unit id="sd1146418b344f81f">
|
||||
<source>Update Notification Rule</source>
|
||||
@ -9242,10 +9232,6 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<source>No app entitlements created.</source>
|
||||
<target>未创建应用程序授权。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sdc8a8f29af6aa411">
|
||||
<source>This application does currently not have any application entitlement defined.</source>
|
||||
<target>此应用程序目前没有定义任何应用程序授权。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sf0bd204ce3fea1de">
|
||||
<source>Create Entitlement</source>
|
||||
<target>创建授权</target>
|
||||
@ -9887,6 +9873,42 @@ Bindings to groups/users are checked against the user of the event.</source>
|
||||
<trans-unit id="s4f820625804ed29b">
|
||||
<source>Re-authenticate with Plex</source>
|
||||
<target>使用 Plex 重新验证身份</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s0433d667ea6eec1a">
|
||||
<source>The name of an invitation must be a slug: only lower case letters, numbers, and the hyphen are permitted here.</source>
|
||||
<target>邀请名称必须是一个 Slug:仅允许小写字母、数字和连字符。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s2e9d5ea88f02ae68">
|
||||
<source>Select the group of users which the alerts are sent to. </source>
|
||||
<target>选择一组用于发送警告的用户。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="se630f2ccd39bf9e6">
|
||||
<source>If no group is selected and 'Send notification to event user' is disabled the rule is disabled. </source>
|
||||
<target>如果未选择组,并且“发送通知给事件用户”被禁用,则此规则被禁用。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s47966b2a708694e2">
|
||||
<source>Send notification to event user</source>
|
||||
<target>发送通知给事件用户</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sd30f00ff2135589c">
|
||||
<source>When enabled, notification will be sent to the user that triggered the event in addition to any users in the group above. The event user will always be the first user, to send a notification only to the event user enabled 'Send once' in the notification transport.</source>
|
||||
<target>启用时,通知不仅会发送给触发事件的用户,还会发送到组中的任何用户。事件用户将总是第一个用户,要只向事件用户发送通知,则需要在通知传输中启用“发送一次”。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sbd65aeeb8a3b9bbc">
|
||||
<source>Maximum registration attempts</source>
|
||||
<target>最大注册尝试次数</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s8495753cb15e8d8e">
|
||||
<source>Maximum allowed registration attempts. When set to 0 attempts, attempts are not limited.</source>
|
||||
<target>允许的最大注册尝试次数。设置为 0 则不限制次数。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="sab4db6a3bd6abc1e">
|
||||
<source>This application does currently not have any application entitlements defined.</source>
|
||||
<target>此应用程序目前没有定义任何应用程序授权。</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="s7225aacf0eee94d2">
|
||||
<source>Authenticated as <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/></source>
|
||||
<target>以 <x id="0" equiv-text="${renderUsername(event.user.authenticated_as)}"/> 身份通过验证</target>
|
||||
</trans-unit>
|
||||
</body>
|
||||
</file>
|
||||
|
@ -45,11 +45,11 @@ Configuration details such as credentials can be specified through _settings_, w
|
||||
|
||||
### Connection settings
|
||||
|
||||
Each connection is authorized through authentik Policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
|
||||
Each connection is authorized through authentik policy objects that are bound to the application and the endpoint. Additional verification can be done with the authorization flow.
|
||||
|
||||
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user/customization.mdx). Once the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually.
|
||||
A new connection is created every time an endpoint is selected in the [User Interface](../../../customize/interfaces/user). After the user's authentik session expires, the connection is terminated. Additionally, the connection timeout can be specified in the provider, which applies even if the user is still authenticated. The connection can also be terminated manually from the **Connections** tab of the RAC provider.
|
||||
|
||||
Additionally it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
Additionally, it is possible to modify the connection settings through the authorization flow. Configuration set in `connection_settings` in the flow plan context will be merged with other settings as shown above.
|
||||
|
||||
The RAC provider utilises [Apache Guacamole](https://guacamole.apache.org/) for establishing SSH, RDP and VNC connections. RAC supports the use of Apache Guacamole connection configurations.
|
||||
|
||||
|
@ -6,8 +6,8 @@ You can customize the behaviour, look, and available resources for your authenti
|
||||
|
||||
- [Policies](./policies/working_with_policies.md)
|
||||
- Interfaces:
|
||||
- [Flows](./interfaces/flow/customization.mdx)
|
||||
- [User interface](./interfaces/user/customization.mdx)
|
||||
- [Admin interface](./interfaces/admin/customization.mdx)
|
||||
- [Flow interface](./interfaces/flow)
|
||||
- [User interface](./interfaces/user)
|
||||
- [Admin interface](./interfaces/admin)
|
||||
- [Blueprints](./blueprints/index.mdx)
|
||||
- [Branding](./branding.md)
|
||||
|
19
website/docs/customize/interfaces/_enabledfeatureslist.mdx
Normal file
19
website/docs/customize/interfaces/_enabledfeatureslist.mdx
Normal file
@ -0,0 +1,19 @@
|
||||
### Enabling/disabling features
|
||||
|
||||
The features listed below can be enabled or disabled through attributes set on the Brand. By default, all of the listed features are enabled. To disable a specific feature, set its value to `false`.
|
||||
|
||||
#### `settings.enabledFeatures.apiDrawer`
|
||||
|
||||
Display the API Request drawer in the upper tool bar.
|
||||
|
||||
#### `settings.enabledFeatures.notificationDrawer`
|
||||
|
||||
Display the Notification drawer in the upper tool bar.
|
||||
|
||||
#### `settings.enabledFeatures.settings`
|
||||
|
||||
Display the Settings link in the upper tool bar.
|
||||
|
||||
#### `settings.enabledFeatures.search`
|
||||
|
||||
Display the Search bar in the upper tool bar.
|
36
website/docs/customize/interfaces/_generalattributes.mdx
Normal file
36
website/docs/customize/interfaces/_generalattributes.mdx
Normal file
@ -0,0 +1,36 @@
|
||||
### General settings (both Admin and User interfaces)
|
||||
|
||||
#### `settings.navbar.userDisplay`
|
||||
|
||||
Configure what is shown in the top right corner. Defaults to `username`. Available options: `username`, `name`, `email`
|
||||
|
||||
#### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme or toggle between dark and light modes. The default setting is `automatic`, which adapts based on the user’s browser preference. Available options: `automatic`, `dark`, `light`.
|
||||
|
||||
**Example**:
|
||||
|
||||
```
|
||||
settings:
|
||||
theme:
|
||||
base: dark
|
||||
```
|
||||
|
||||
#### `settings.theme.background`
|
||||
|
||||
Optional CSS that is applied to the background of the User interface, for example to set a custom background color, gradient, or image.
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
theme:
|
||||
background: >
|
||||
background: url('https://picsum.photos/1920/1080');
|
||||
filter: blur(8px);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
```
|
||||
|
||||
#### `settings.locale`
|
||||
|
||||
The locale which can be configured in the user settings by default. This can be used to preset locales for groups of users, but still let them choose their own preferred locale.
|
@ -1,3 +1,11 @@
|
||||
### Global customization
|
||||
## Global customization
|
||||
|
||||
See [Brand Settings](../../../sys-mgmt/brands.md#branding-settings)
|
||||
To customize the following brand settings, log in to the Admin interface and navigate to **System > Brands > Brand settings**.
|
||||
|
||||
- Title
|
||||
- Logo
|
||||
- Favicon
|
||||
- Default flow background image
|
||||
- Custom CSS
|
||||
|
||||
For more details, see the [Brand settings](../../../sys-mgmt/brands.md#branding-settings) documentation.
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 96 KiB |
@ -1,17 +0,0 @@
|
||||
# Customization
|
||||
|
||||
### `settings.pagination.perPage`
|
||||
|
||||
How many items should be retrieved per page. Defaults to 20.
|
||||
|
||||
### `settings.defaults.userPath`
|
||||
|
||||
Default user path which is opened when opening the user list. Defaults to `users`.
|
||||
|
||||
### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
46
website/docs/customize/interfaces/admin/index.mdx
Normal file
46
website/docs/customize/interfaces/admin/index.mdx
Normal file
@ -0,0 +1,46 @@
|
||||
---
|
||||
title: Customize the Admin interface
|
||||
sidebar_label: Admin interface
|
||||
---
|
||||
|
||||
The Admin interface can be customized using attributes configured in [Brands](../../../sys-mgmt/brands.md)
|
||||
|
||||
To add, remove, or modify attributes for a brand, log in to the Admin interface and navigate to **System > Brands > Other global settings > Attributes**.
|
||||
|
||||
Most attributes defined in a brand apply to _both_ the User and Admin interfaces. However, any settings that are specific to only the Admin interface are explicitly noted as such below.
|
||||
|
||||
The following screenshot shows the syntax for setting several attributes for a brand: dark mode, a 3-column display of applications on **My applications** page of the User interface, and hiding the API and Notifications drawers from the Admin interface tool bar.
|
||||
|
||||

|
||||
|
||||
## Custom settings
|
||||
|
||||
The following settings for attributes are grouped by:
|
||||
|
||||
- `enabledFeatures` settings
|
||||
- General settings (used on both the Admin interface and the User interface)
|
||||
- Admin interface only
|
||||
|
||||
import Enabledfeatureslist from "../\_enabledfeatureslist.mdx";
|
||||
|
||||
<Enabledfeatureslist />
|
||||
|
||||
import Generalattributes from "../\_generalattributes.mdx";
|
||||
|
||||
<Generalattributes />
|
||||
|
||||
### Settings for the Admin interface only
|
||||
|
||||
The following settings can only be used to customize the Admin interface, not the User interface.
|
||||
|
||||
#### `settings.pagination.perPage`
|
||||
|
||||
How many items should be retrieved per page. Defaults to 20.
|
||||
|
||||
#### `settings.defaults.userPath`
|
||||
|
||||
Default user path which is used when opening the user list. Defaults to `users`.
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
@ -1,11 +0,0 @@
|
||||
# Customization
|
||||
|
||||
Since flows can be executed authenticated or unauthenticated, the default settings can be set via brands _attributes_.
|
||||
|
||||
### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
19
website/docs/customize/interfaces/flow/index.mdx
Normal file
19
website/docs/customize/interfaces/flow/index.mdx
Normal file
@ -0,0 +1,19 @@
|
||||
---
|
||||
title: Customize a flow
|
||||
sidebar_label: Flow interface
|
||||
---
|
||||
|
||||
Typically, settings for flows are defined as defaults in the [Brand settings](../../../sys-mgmt/brands.md). However, it’s important to note that some flows are executed before the specific user is authenticated and thus before authentik can determine which user is viewing the flow (for example, the `default-authentication-flow`!). Consequently, using default settings for all flows ensures a more consistent user experience.
|
||||
|
||||
Two settings that you can configure per flow are the _background image_ for the flow, and the _layout_.
|
||||
|
||||
## Customize a flow's background image
|
||||
|
||||
You can define a:
|
||||
|
||||
- Default background image for all flows, set in the instance's [brand](../../../sys-mgmt/brands.md)
|
||||
- A background image for [one or more specific flows](../../../add-secure-apps/flows-stages/flow/index.md#flow-configuration-options) (overrides the default)
|
||||
|
||||
## Set the layout for a flow
|
||||
|
||||
To define the layout for a flow, edit the flow and under **Appearance settings > Layout** select how the UI displays the flow when it is executed; with stacked elements, content left or right, and sidebar left or right.
|
@ -1,64 +0,0 @@
|
||||
# Customization
|
||||
|
||||
The user interface can be customized through attributes, and will be inherited from a users' groups.
|
||||
|
||||
## Enabling/disabling features
|
||||
|
||||
The following features can be enabled/disabled. By default, all of them are enabled:
|
||||
|
||||
- `settings.enabledFeatures.apiDrawer`
|
||||
|
||||
API Request drawer in navbar
|
||||
|
||||
- `settings.enabledFeatures.notificationDrawer`
|
||||
|
||||
Notification drawer in navbar
|
||||
|
||||
- `settings.enabledFeatures.settings`
|
||||
|
||||
Settings link in navbar
|
||||
|
||||
- `settings.enabledFeatures.applicationEdit`
|
||||
|
||||
Application edit in library (only shown when user is superuser)
|
||||
|
||||
- `settings.enabledFeatures.search`
|
||||
|
||||
Search bar
|
||||
|
||||
## Other configuration
|
||||
|
||||
### `settings.navbar.userDisplay`
|
||||
|
||||
Configure what is shown in the top right corner. Defaults to `username`. Choices: `username`, `name`, `email`
|
||||
|
||||
### `settings.theme.base`
|
||||
|
||||
Configure the base color scheme. Defaults to `automatic`, which switches between dark and light mode based on the users' browsers' preference. Choices: `automatic`, `dark`, `light`.
|
||||
|
||||
### `settings.theme.background`
|
||||
|
||||
Optional CSS which is applied in the background of the background of the user interface; for example
|
||||
|
||||
```yaml
|
||||
settings:
|
||||
theme:
|
||||
background: >
|
||||
background: url('https://picsum.photos/1920/1080');
|
||||
filter: blur(8px);
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
```
|
||||
|
||||
### `settings.layout.type`
|
||||
|
||||
Which layout to use for the _My applications_ view. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
|
||||
|
||||
### `settings.locale`
|
||||
|
||||
The locale which can be configured in the user settings by default. This can be used to preset locales for groups of users, but still let them choose their own preferred locale
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
44
website/docs/customize/interfaces/user/index.mdx
Normal file
44
website/docs/customize/interfaces/user/index.mdx
Normal file
@ -0,0 +1,44 @@
|
||||
---
|
||||
title: Customize the User interface
|
||||
sidebar_label: User interface
|
||||
---
|
||||
|
||||
The User interface can be customized using attributes configured in [Brands](../../../sys-mgmt/brands.md).
|
||||
|
||||
To add, remove, or modify attributes for a brand, log in as an administrator and navigate to **System > Brands > Other global settings > Attributes**.
|
||||
|
||||
Most attributes defined in a brand apply to _both_ the User and Admin interfaces. However, any settings that are specific to only one interface are explicitly noted as such below.
|
||||
|
||||
The following screenshot shows the syntax for setting several attributes for a brand: light mode, a 3-column display of applications on **My applications** page, hiding the API drawer and the Notification drawer from the tool bar, and disallowing users to edit the applications on **My applications** page.
|
||||
|
||||

|
||||
|
||||
## Custom settings
|
||||
|
||||
The following settings for attributes are grouped by:
|
||||
|
||||
- `enabledFeatures` settings
|
||||
- General attributes (used on both the Admin interface and the User interface)
|
||||
- User interface only
|
||||
|
||||
import Enabledfeatureslist from "../\_enabledfeatureslist.mdx";
|
||||
|
||||
<Enabledfeatureslist />
|
||||
|
||||
#### `settings.enabledFeatures.applicationEdit` (User interface only)
|
||||
|
||||
Display the Edit option for each application on the **My applications** page (only shown when user is superuser).
|
||||
|
||||
import Generalattributes from "../\_generalattributes.mdx";
|
||||
|
||||
<Generalattributes />
|
||||
|
||||
### Settings for the User interface only
|
||||
|
||||
#### `settings.layout.type`
|
||||
|
||||
Which layout to use for the **My applications** page. Defaults to `row`. Choices: `row`, `2-column`, `3-column`
|
||||
|
||||
import Global from "../_global/global.mdx";
|
||||
|
||||
<Global />
|
Binary file not shown.
After Width: | Height: | Size: 106 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user