Skip to content

Commit

Permalink
Merge branch 'main' into web/lint/lint-lockfile
Browse files Browse the repository at this point in the history
* main: (81 commits)
  web: provide a test framework (#9681)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#10272)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#10271)
  core: bump drf-jsonschema-serializer from 2.0.0 to 3.0.0 (#10262)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#10268)
  core: bump goauthentik.io/api/v3 from 3.2024042.13 to 3.2024060.1 (#10260)
  core, web: update translations (#10259)
  website/docs: update geoip and asn documentation following field changes (#10265)
  web: provide better feedback on Application Library page about search results (#9386)
  web: disable reading dark mode out of the UI by default (#10256)
  web/flows: remove continue button from AutoSubmit stage (#10253)
  web: bump API Client version (#10252)
  website/docs: update geoip and asn example to use the proper syntax (cherry-pick #10249) (#10250)
  website/docs: update the Welcome page (#10222)
  website/docs: update geoip and asn example to use the proper syntax (#10249)
  security: update supported versions (cherry-pick #10247) (#10248)
  security: update supported versions (#10247)
  website/docs: remove RC disclaimer from 2024.6 release notes (cherry-pick #10245) (#10246)
  website/docs: remove RC disclaimer from 2024.6 release notes (#10245)
  website/docs: update 2024.6 release notes with latest changes (cherry-pick #10228) (#10243)
  ...
  • Loading branch information
kensternberg-authentik committed Jun 27, 2024
2 parents e411739 + 861992f commit 19713e2
Show file tree
Hide file tree
Showing 95 changed files with 16,062 additions and 14,525 deletions.
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.4.2
current_version = 2024.6.0
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*))?
Expand Down
18 changes: 18 additions & 0 deletions .github/workflows/ci-web.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,21 @@ jobs:
- name: build
working-directory: web/
run: npm run build
test:
needs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test
4 changes: 2 additions & 2 deletions .github/workflows/release-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,8 @@ jobs:
- uses: actions/checkout@v4
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ jobs:
- uses: actions/checkout@v4
- name: Pre-release test
run: |
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
docker buildx install
mkdir -p ./gen-ts-api
docker build -t testing:latest .
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as web-builder

ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
ENV NODE_ENV=production

WORKDIR /work/web
Expand Down
10 changes: 6 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ test-go:
go test -timeout 0 -v -race -cover ./...

test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(shell openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64)" >> .env
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis
Expand All @@ -60,9 +60,11 @@ test: ## Run the server tests and produce a coverage report (locally)
coverage html
coverage report

lint-fix: ## Lint and automatically fix errors in the python source code. Reports spelling errors.
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
black $(PY_SOURCES)
ruff check --fix $(PY_SOURCES)

lint-codespell: ## Reports spelling errors.
codespell -w $(CODESPELL_ARGS)

lint: ## Lint the python and golang sources
Expand Down Expand Up @@ -239,7 +241,7 @@ website: website-lint-fix website-build ## Automatically fix formatting issues
website-install:
cd website && npm ci

website-lint-fix:
website-lint-fix: lint-codespell
cd website && npm run prettier

website-build:
Expand Down
8 changes: 4 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni

(.x being the latest patch release for each version)

| Version | Supported |
| --------- | --------- |
| 2023.10.x ||
| 2024.2.x ||
| Version | Supported |
| -------- | --------- |
| 2024.4.x ||
| 2024.6.x ||

## Reporting a Vulnerability

Expand Down
2 changes: 1 addition & 1 deletion authentik/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from os import environ

__version__ = "2024.4.2"
__version__ = "2024.6.0"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"


Expand Down
7 changes: 7 additions & 0 deletions authentik/core/api/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,13 @@ def __init__(self, *args, **kwargs) -> None:
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)

def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user

def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")
Expand Down
7 changes: 5 additions & 2 deletions authentik/core/expression/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,11 @@ def handle_error(self, exc: Exception, expression_source: str):
)
if "request" in self._context:
req: PolicyRequest = self._context["request"]
event.from_http(req.http_request, req.user)
return
if req.http_request:
event.from_http(req.http_request, req.user)
return
elif req.user:
event.set_user(req.user)
event.save()

def evaluate(self, *args, **kwargs) -> Any:
Expand Down
3 changes: 2 additions & 1 deletion authentik/core/expression/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""authentik core exceptions"""

from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sentry import SentryIgnoredException


Expand All @@ -12,7 +13,7 @@ def __init__(self, exc: Exception, mapping) -> None:
self.mapping = mapping


class SkipObjectException(PropertyMappingExpressionException):
class SkipObjectException(ControlFlowException):
"""Exception which can be raised in a property mapping to skip syncing an object.
Only applies to Property mappings which sync objects, and not on mappings which transitively
apply to a single user"""
3 changes: 3 additions & 0 deletions authentik/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.generators import generate_id
from authentik.lib.models import (
CreatedUpdatedModel,
Expand Down Expand Up @@ -783,6 +784,8 @@ def evaluate(self, user: User | None, request: HttpRequest | None, **kwargs) ->
evaluator = PropertyMappingEvaluator(self, user, request, **kwargs)
try:
return evaluator.evaluate(self.expression)
except ControlFlowException as exc:
raise exc
except Exception as exc:
raise PropertyMappingExpressionException(self, exc) from exc

Expand Down
16 changes: 15 additions & 1 deletion authentik/core/tests/test_property_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user

from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
Expand Down Expand Up @@ -42,6 +45,17 @@ def test_expression_error_general(self):
self.assertTrue(events.exists())
self.assertEqual(len(events), 1)

def test_expression_skip(self):
"""Test expression error"""
expr = "raise SkipObject"
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
with self.assertRaises(SkipObjectException):
mapping.evaluate(None, None)
events = Event.objects.filter(
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
)
self.assertFalse(events.exists())

def test_expression_error_extended(self):
"""Test expression error (with user and http request"""
expr = "return aaa"
Expand Down
23 changes: 20 additions & 3 deletions authentik/core/tests/test_token_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
)
from authentik.core.tests.utils import create_test_admin_user
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id


Expand All @@ -24,7 +23,7 @@ class TestTokenAPI(APITestCase):

def setUp(self) -> None:
super().setUp()
self.user = User.objects.create(username="testuser")
self.user = create_test_user()
self.admin = create_test_admin_user()
self.client.force_login(self.user)

Expand Down Expand Up @@ -154,6 +153,24 @@ def test_token_create_expiring_custom_api(self):
self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())

def test_token_change_user(self):
"""Test creating a token and then changing the user"""
ident = generate_id()
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier=ident)
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
response = self.client.put(
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
)
self.assertEqual(response.status_code, 400)
token.refresh_from_db()
self.assertEqual(token.user, self.user)

def test_list(self):
"""Test Token List (Test normal authentication)"""
Token.objects.all().delete()
Expand Down
4 changes: 3 additions & 1 deletion authentik/lib/expression/evaluator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
Expand Down Expand Up @@ -216,7 +217,8 @@ def evaluate(self, expression_source: str) -> Any:
# so the user only sees information relevant to them
# and none of our surrounding error handling
exc.__traceback__ = exc.__traceback__.tb_next
self.handle_error(exc, expression_source)
if not isinstance(exc, ControlFlowException):
self.handle_error(exc, expression_source)
raise exc
return result

Expand Down
6 changes: 6 additions & 0 deletions authentik/lib/expression/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from authentik.lib.sentry import SentryIgnoredException


class ControlFlowException(SentryIgnoredException):
"""Exceptions used to control the flow from exceptions, not reported as a warning/
error in logs"""
7 changes: 5 additions & 2 deletions authentik/lib/sync/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
from django.http import HttpRequest

from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
)
from authentik.core.models import PropertyMapping, User
from authentik.lib.expression.exceptions import ControlFlowException


class PropertyMappingManager:
Expand Down Expand Up @@ -57,7 +60,7 @@ def iter_eval(
mapping.set_context(user, request, **kwargs)
try:
value = mapping.evaluate(mapping.model.expression)
except PropertyMappingExpressionException as exc:
except (PropertyMappingExpressionException, ControlFlowException) as exc:
raise exc from exc
except Exception as exc:
raise PropertyMappingExpressionException(exc, mapping.model) from exc
Expand Down
4 changes: 2 additions & 2 deletions authentik/lib/sync/outgoing/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync
from authentik.lib.utils.errors import exception_to_string
Expand Down Expand Up @@ -92,7 +92,7 @@ def to_schema(self, obj: TModel, connection: TConnection | None, **defaults) ->
eval_kwargs.setdefault("user", None)
for value in self.mapper.iter_eval(**eval_kwargs):
always_merger.merge(raw_final_object, value)
except SkipObjectException as exc:
except ControlFlowException as exc:
raise exc from exc
except PropertyMappingExpressionException as exc:
# Value error can be raised when assigning invalid data to an attribute
Expand Down
23 changes: 22 additions & 1 deletion authentik/providers/oauth2/tests/test_device_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@

from django.urls import reverse

from authentik.core.models import Application
from authentik.core.models import Application, Group
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
Expand Down Expand Up @@ -77,3 +78,23 @@ def test_device_init_qs(self):
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code}),
)

def test_device_init_denied(self):
"""Test device init"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.application,
order=0,
)
token = DeviceToken.objects.create(
user_code="foo",
provider=self.provider,
)
res = self.client.get(
reverse("authentik_providers_oauth2_root:device-login")
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code})
)
self.assertEqual(res.status_code, 200)
self.assertIn(b"Permission denied", res.content)
7 changes: 5 additions & 2 deletions authentik/providers/oauth2/views/device_backchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@
from rest_framework.throttling import AnonRateThrottle
from structlog.stdlib import get_logger

from authentik.core.models import Application
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE

LOGGER = get_logger()

Expand All @@ -37,7 +38,9 @@ def parse_request(self) -> HttpResponse | None:
).first()
if not provider:
return HttpResponseBadRequest()
if not get_application(provider):
try:
_ = provider.application
except Application.DoesNotExist:
return HttpResponseBadRequest()
self.provider = provider
self.client_id = client_id
Expand Down
Loading

0 comments on commit 19713e2

Please sign in to comment.