Skip to content

Commit

Permalink
Merge branch 'main' into web/automated-testing
Browse files Browse the repository at this point in the history
* main: (68 commits)
  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)
  website/docs: update 2024.4 release notes with latest changes (#10231)
  website/docs: update 2024.2 release notes with security fixes (#10232)
  website/docs: update 2024.6 release notes with latest changes (#10228)
  release: 2024.6.0
  security: fix CVE-2024-37905 (cherry-pick #10230) (#10237)
  core: bump pdoc from 14.5.0 to 14.5.1 (#10221)
  web: bump chromedriver from 126.0.3 to 126.0.4 in /tests/wdio (#10223)
  core: bump webauthn from 2.1.0 to 2.2.0 (#10224)
  web: bump @sentry/browser from 8.11.0 to 8.12.0 in /web in the sentry group (#10226)
  core: bump debugpy from 1.8.1 to 1.8.2 (#10225)
  security: fix CVE-2024-37905 (#10230)
  ...
  • Loading branch information
kensternberg-authentik committed Jun 26, 2024
2 parents ea71d41 + 632f098 commit cc2cf47
Show file tree
Hide file tree
Showing 58 changed files with 3,053 additions and 936 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
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
4 changes: 2 additions & 2 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 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 cc2cf47

Please sign in to comment.