Compare commits

..

19 Commits

Author SHA1 Message Date
63cfbb721c release: 2023.2.3 2023-03-02 20:17:51 +01:00
2b74a1f03b security: fix CVE-2023-26481 (#4832)
fix CVE-2023-26481

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-03-02 20:16:29 +01:00
093573f89a website: always show build version in version dropdown
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#3940
2023-02-16 14:39:17 +01:00
d842fc4958 release: 2023.2.2 2023-02-15 19:53:42 +01:00
19f5e6e07e website/docs: update events page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:44:13 +01:00
acfa9c76d1 providers/ldap: check MFA password on password stage
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:27:08 +01:00
bff34cc5dc root: use channel send workaround for sync sending of websocket messages
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:08:01 +01:00
7f009f6d02 flows: include flow authentication requirement in diagram
closes #4533

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 16:04:45 +01:00
dfb9ae548c web/admin: fix error when creating new users
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

#4685
2023-02-15 15:32:48 +01:00
7d6b573f8b website: migrate to mermaid charts, rework proxy page
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 12:14:17 +01:00
ade397fc24 web/user: revert truncate behaviour for application description
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 11:17:45 +01:00
d945d30cda providers/proxy: fix value is too long with filesystem sessions
closes #4693

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 10:50:01 +01:00
c8c401e2c5 lib: don't try to cache generated avatar with full user, only cache with name
closes #4690

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 10:49:13 +01:00
e4ca20bfc6 core: bump golang from 1.20.0-bullseye to 1.20.1-bullseye (#4691)
Bumps golang from 1.20.0-bullseye to 1.20.1-bullseye.

---
updated-dependencies:
- dependency-name: golang
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 10:46:02 +01:00
6347716815 core: bump goauthentik.io/api/v3 from 3.2023012.5 to 3.2023021.1 (#4692)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2023012.5 to 3.2023021.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2023012.5...v3.2023021.1)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-15 10:45:20 +01:00
859b6cc60e website: adjust padding on hero header
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:28:45 +01:00
06a1a7f076 ci: add time limits to ci jobs
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:28:36 +01:00
b6c120f555 providers/proxy: fix client credential flows not using http interceptor
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-02-15 00:22:56 +01:00
6cc363bc5d web: bump API Client version (#4689)
Signed-off-by: GitHub <noreply@github.com>
2023-02-14 20:35:22 +01:00
42 changed files with 1495 additions and 153 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2023.2.1
current_version = 2023.2.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)

View File

@ -80,6 +80,7 @@ jobs:
run: poetry run python -m lifecycle.migrate
test-unittest:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Setup authentik env
@ -94,6 +95,7 @@ jobs:
flags: unit
test-integration:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v3
- name: Setup authentik env
@ -111,6 +113,7 @@ jobs:
test-e2e:
name: test-e2e (${{ matrix.job.name }})
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:

View File

@ -31,7 +31,7 @@ RUN pip install --no-cache-dir poetry && \
poetry export -f requirements.txt --dev --output requirements-dev.txt
# Stage 4: Build go proxy
FROM docker.io/golang:1.20.0-bullseye AS go-builder
FROM docker.io/golang:1.20.1-bullseye AS go-builder
WORKDIR /work

View File

@ -2,7 +2,7 @@
from os import environ
from typing import Optional
__version__ = "2023.2.1"
__version__ = "2023.2.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -8,7 +8,7 @@ from rest_framework.serializers import CharField
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User
from authentik.flows.models import Flow, FlowStageBinding
from authentik.flows.models import Flow, FlowAuthenticationRequirement, FlowStageBinding
@dataclass
@ -160,12 +160,37 @@ class FlowDiagram:
)
return stages + elements
def get_flow_auth_requirement(self) -> list[DiagramElement]:
"""Get flow authentication requirement"""
end_el = DiagramElement(
"done",
_("End of the flow"),
_("Requirement not fulfilled"),
style=["[[", "]]"],
)
elements = []
if self.flow.authentication == FlowAuthenticationRequirement.NONE:
return []
auth = DiagramElement(
"flow_auth_requirement",
_("Flow authentication requirement") + "\n" + self.flow.authentication,
)
elements.append(auth)
end_el.source = [auth]
elements.append(end_el)
elements.append(
DiagramElement("flow_start", "placeholder", _("Requirement fulfilled"), source=[auth])
)
return elements
def build(self) -> str:
"""Build flowchart"""
all_elements = [
"graph TD",
]
all_elements.extend(self.get_flow_auth_requirement())
pre_flow_policies_element = DiagramElement(
"flow_pre", _("Pre-flow policies"), style=["[[", "]]"]
)
@ -179,6 +204,7 @@ class FlowDiagram:
_("End of the flow"),
_("Policy denied"),
flow_policies,
style=["[[", "]]"],
)
)

View File

@ -162,7 +162,7 @@ class FlowExecutorView(APIView):
token.delete()
if not isinstance(plan, FlowPlan):
return None
plan.context[PLAN_CONTEXT_IS_RESTORED] = True
plan.context[PLAN_CONTEXT_IS_RESTORED] = token
self._logger.debug("f(exec): restored flow plan from token", plan=plan)
return plan

View File

@ -86,7 +86,7 @@ def generate_colors(text: str) -> tuple[str, str]:
@cache
# pylint: disable=too-many-arguments,too-many-locals
def generate_avatar_from_name(
user: "User",
name: str,
length: int = 2,
size: int = 64,
rounded: bool = False,
@ -98,8 +98,6 @@ def generate_avatar_from_name(
Inspired from: https://github.com/LasseRafn/ui-avatars
"""
name = user.name if user.name != "" else "a k"
name_parts = name.split()
# Only abbreviate first and last name
if len(name_parts) > 2:
@ -152,7 +150,7 @@ def generate_avatar_from_name(
def avatar_mode_generated(user: "User", mode: str) -> Optional[str]:
"""Wrapper that converts generated avatar to base64 svg"""
svg = generate_avatar_from_name(user)
svg = generate_avatar_from_name(user.name if user.name != "" else "a k")
return f"data:image/svg+xml;base64,{b64encode(svg.encode('utf-8')).decode('utf-8')}"

View File

@ -7,7 +7,6 @@ from urllib.parse import urlparse
import yaml
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from django.core.cache import cache
from django.db import DatabaseError, InternalError, ProgrammingError
from django.db.models.base import Model
@ -43,6 +42,7 @@ from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesContro
from authentik.providers.proxy.controllers.docker import ProxyDockerController
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
from authentik.root.celery import CELERY_APP
from authentik.root.messages.storage import closing_send
LOGGER = get_logger()
CACHE_KEY_OUTPOST_DOWN = "outpost_teardown_%s"
@ -217,26 +217,23 @@ def outpost_post_save(model_class: str, model_pk: Any):
def outpost_send_update(model_instace: Model):
"""Send outpost update to all registered outposts, regardless to which authentik
instance they are connected"""
channel_layer = get_channel_layer()
if isinstance(model_instace, OutpostModel):
for outpost in model_instace.outpost_set.all():
_outpost_single_update(outpost, channel_layer)
_outpost_single_update(outpost)
elif isinstance(model_instace, Outpost):
_outpost_single_update(model_instace, channel_layer)
_outpost_single_update(model_instace)
def _outpost_single_update(outpost: Outpost, layer=None):
def _outpost_single_update(outpost: Outpost):
"""Update outpost instances connected to a single outpost"""
# Ensure token again, because this function is called when anything related to an
# OutpostModel is saved, so we can be sure permissions are right
_ = outpost.token
outpost.build_user_permissions(outpost.user)
if not layer: # pragma: no cover
layer = get_channel_layer()
for state in OutpostState.for_outpost(outpost):
for channel in state.channel_ids:
LOGGER.debug("sending update", channel=channel, instance=state.uid, outpost=outpost)
async_to_sync(layer.send)(channel, {"type": "event.update"})
async_to_sync(closing_send)(channel, {"type": "event.update"})
@CELERY_APP.task()

View File

@ -1,6 +1,7 @@
"""Channels Messages storage"""
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from channels import DEFAULT_CHANNEL_LAYER
from channels.layers import channel_layers
from django.contrib.messages.storage.base import Message
from django.contrib.messages.storage.session import SessionStorage
from django.core.cache import cache
@ -10,13 +11,21 @@ SESSION_KEY = "_messages"
CACHE_PREFIX = "goauthentik.io/root/messages_"
async def closing_send(channel, message):
"""Wrapper around layer send that closes the connection"""
# See https://github.com/django/channels_redis/issues/332
# TODO: Remove this after channels_redis 4.1 is released
channel_layer = channel_layers.make_backend(DEFAULT_CHANNEL_LAYER)
await channel_layer.send(channel, message)
await channel_layer.close_pools()
class ChannelsStorage(SessionStorage):
"""Send contrib.messages over websocket"""
def __init__(self, request: HttpRequest) -> None:
# pyright: reportGeneralTypeIssues=false
super().__init__(request)
self.channel = get_channel_layer()
def _store(self, messages: list[Message], response, *args, **kwargs):
prefix = f"{CACHE_PREFIX}{self.request.session.session_key}_messages_"
@ -28,7 +37,7 @@ class ChannelsStorage(SessionStorage):
for key in keys:
uid = key.replace(prefix, "")
for message in messages:
async_to_sync(self.channel.send)(
async_to_sync(closing_send)(
uid,
{
"type": "event.update",

View File

@ -15,7 +15,7 @@ from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTyp
from authentik.flows.models import FlowToken
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import QS_KEY_TOKEN, SESSION_KEY_GET
from authentik.flows.views.executor import QS_KEY_TOKEN
from authentik.stages.email.models import EmailStage
from authentik.stages.email.tasks import send_mails
from authentik.stages.email.utils import TemplateEmailMessage
@ -103,12 +103,14 @@ class EmailStageView(ChallengeStageView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
# Check if the user came back from the email link to verify
if QS_KEY_TOKEN in request.session.get(
SESSION_KEY_GET, {}
) and self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, False):
restore_token: FlowToken = self.executor.plan.context.get(PLAN_CONTEXT_IS_RESTORED, None)
user = self.get_pending_user()
if restore_token:
if restore_token.user != user:
self.logger.warning("Flow token for non-matching user, denying request")
return self.executor.stage_invalid()
messages.success(request, _("Successfully verified Email."))
if self.executor.current_stage.activate_user_on_success:
user = self.get_pending_user()
user.is_active = True
user.save()
return self.executor.stage_ok()

View File

@ -7,10 +7,9 @@ from django.core.mail.backends.smtp import EmailBackend as SMTPEmailBackend
from django.urls import reverse
from django.utils.http import urlencode
from authentik.core.models import Token
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
from authentik.flows.markers import StageMarker
from authentik.flows.models import FlowDesignation, FlowStageBinding
from authentik.flows.models import FlowDesignation, FlowStageBinding, FlowToken
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from authentik.flows.tests import FlowTestCase
from authentik.flows.views.executor import SESSION_KEY_PLAN
@ -134,7 +133,7 @@ class TestEmailStage(FlowTestCase):
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
token: Token = Token.objects.get(user=self.user)
token: FlowToken = FlowToken.objects.get(user=self.user)
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session
@ -165,3 +164,43 @@ class TestEmailStage(FlowTestCase):
plan: FlowPlan = session[SESSION_KEY_PLAN]
self.assertEqual(plan.context[PLAN_CONTEXT_PENDING_USER], self.user)
self.assertTrue(plan.context[PLAN_CONTEXT_PENDING_USER].is_active)
def test_token_invalid_user(self):
"""Test with token with invalid user"""
# Make sure token exists
self.test_pending_user()
self.user.is_active = False
self.user.save()
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
session = self.client.session
session[SESSION_KEY_PLAN] = plan
session.save()
# Set flow token user to a different user
token: FlowToken = FlowToken.objects.get(user=self.user)
token.user = create_test_admin_user()
token.save()
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
# Call the executor shell to preseed the session
url = reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
)
url_query = urlencode(
{
QS_KEY_TOKEN: token.key,
}
)
url += f"?query={url_query}"
self.client.get(url)
# Call the actual executor to get the JSON Response
response = self.client.get(
reverse(
"authentik_api:flow-executor",
kwargs={"flow_slug": self.flow.slug},
)
)
self.assertEqual(response.status_code, 200)
self.assertStageResponse(response, component="ak-stage-access-denied")

View File

@ -154,6 +154,7 @@ entries:
policy: !KeyOf default-recovery-skip-if-restored
target: !KeyOf flow-binding-email
order: 0
state: absent
model: authentik_policies.policybinding
attrs:
negate: false

View File

@ -32,7 +32,7 @@ services:
volumes:
- redis:/data
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.3}
restart: unless-stopped
command: server
environment:
@ -50,7 +50,7 @@ services:
- "${AUTHENTIK_PORT_HTTP:-9000}:9000"
- "${AUTHENTIK_PORT_HTTPS:-9443}:9443"
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.1}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2023.2.3}
restart: unless-stopped
command: worker
environment:

2
go.mod
View File

@ -25,7 +25,7 @@ require (
github.com/prometheus/client_golang v1.14.0
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.1
goauthentik.io/api/v3 v3.2023012.5
goauthentik.io/api/v3 v3.2023021.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f

9
go.sum
View File

@ -380,10 +380,8 @@ go.opentelemetry.io/otel/sdk v1.11.1 h1:F7KmQgoHljhUuJyA+9BiU+EkJfyX5nVVF4wyzWZp
go.opentelemetry.io/otel/trace v1.11.1 h1:ofxdnzsNrGBYXbP7t7zpUK281+go5rF7dvdIZXF8gdQ=
go.opentelemetry.io/otel/trace v1.11.1/go.mod h1:f/Q9G7vzk5u91PhbmKbg1Qn0rzH1LJ4vbPHFGkTPtOk=
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
goauthentik.io/api/v3 v3.2023012.4 h1:Vn85T5WQOEZfyimcAA6Anf4f+S68Lw3Ap5+v9d4+hZA=
goauthentik.io/api/v3 v3.2023012.4/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
goauthentik.io/api/v3 v3.2023012.5 h1:MRFw5LuRboeb3tD3gl1yRBuAwBLWfOdO9EGXcn/5yIk=
goauthentik.io/api/v3 v3.2023012.5/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
goauthentik.io/api/v3 v3.2023021.1 h1:UH9MiG9tRvbsV4UeCqvtjxxdl69Z3nIMsulwk9Sn1zM=
goauthentik.io/api/v3 v3.2023021.1/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@ -528,8 +526,6 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -590,7 +586,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064 h1:BmCFkEH4nJrYcAc2L08yX5RhYGD4j58PTMkEUDkpz2I=
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@ -29,4 +29,4 @@ func UserAgent() string {
return fmt.Sprintf("authentik@%s", FullVersion())
}
const VERSION = "2023.2.1"
const VERSION = "2023.2.3"

View File

@ -25,6 +25,7 @@ func (fe *FlowExecutor) solveChallenge_Identification(challenge *api.ChallengeTy
}
func (fe *FlowExecutor) solveChallenge_Password(challenge *api.ChallengeTypes, req api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
fe.checkPasswordMFA()
r := api.NewPasswordChallengeResponseRequest(fe.getAnswer(StagePassword))
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
}

View File

@ -36,7 +36,7 @@ func (a *Application) attemptBasicAuth(username, password string) *Claims {
return nil
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := a.httpClient.Do(req)
res, err := a.publicHostHTTPClient.Do(req)
if err != nil || res.StatusCode > 200 {
b, err := io.ReadAll(res.Body)
if err != nil {

View File

@ -39,7 +39,7 @@ func (a *Application) attemptBearerAuth(token string) *TokenIntrospectionRespons
return nil
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
res, err := a.httpClient.Do(req)
res, err := a.publicHostHTTPClient.Do(req)
if err != nil || res.StatusCode > 200 {
a.log.WithError(err).Warning("failed to send introspection request")
return nil

View File

@ -1,6 +1,8 @@
package codecs
import (
"math"
"github.com/gorilla/securecookie"
log "github.com/sirupsen/logrus"
)
@ -12,6 +14,7 @@ type Codec struct {
func New(maxAge int, hashKey, blockKey []byte) *Codec {
cookie := securecookie.New(hashKey, blockKey)
cookie.MaxAge(maxAge)
cookie.MaxLength(math.MaxInt)
return &Codec{
SecureCookie: cookie,
}

View File

@ -1,5 +1,5 @@
# Stage 1: Build
FROM docker.io/golang:1.20.0-bullseye AS builder
FROM docker.io/golang:1.20.1-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View File

@ -7,7 +7,7 @@ ENV NODE_ENV=production
RUN cd /static && npm ci && npm run build-proxy
# Stage 2: Build
FROM docker.io/golang:1.20.0-bullseye AS builder
FROM docker.io/golang:1.20.1-bullseye AS builder
WORKDIR /go/src/goauthentik.io

View File

@ -1,8 +1,5 @@
[tool.pyright]
ignore = [
"**/migrations/**",
"**/node_modules/**"
]
ignore = ["**/migrations/**", "**/node_modules/**"]
reportMissingTypeStubs = false
strictParameterNoneValue = true
strictDictionaryInference = true
@ -63,14 +60,7 @@ exclude_lines = [
show_missing = true
[tool.pylint.basic]
good-names = [
"pk",
"id",
"i",
"j",
"k",
"_",
]
good-names = ["pk", "id", "i", "j", "k", "_"]
[tool.pylint.master]
disable = [
@ -85,6 +75,7 @@ disable = [
"protected-access",
"unused-argument",
"raise-missing-from",
"fixme",
# To preserve django's translation function we need to use %-formatting
"consider-using-f-string",
]
@ -114,13 +105,13 @@ filterwarnings = [
[tool.poetry]
name = "authentik"
version = "2023.2.1"
version = "2023.2.3"
description = ""
authors = ["authentik Team <hello@goauthentik.io>"]
[tool.poetry.dependencies]
celery = "*"
channels = {version = "*", extras = ["daphne"]}
channels = { version = "*", extras = ["daphne"] }
channels-redis = "*"
codespell = "*"
colorama = "*"
@ -147,7 +138,7 @@ gunicorn = "*"
kubernetes = "*"
ldap3 = "*"
lxml = "*"
opencontainers = {extras = ["reggie"],version = "*"}
opencontainers = { extras = ["reggie"], version = "*" }
packaging = "*"
paramiko = "*"
psycopg2-binary = "*"
@ -163,8 +154,8 @@ swagger-spec-validator = "*"
twilio = "*"
twisted = "*"
ua-parser = "*"
urllib3 = {extras = ["secure"],version = "*"}
uvicorn = {extras = ["standard"],version = "*"}
urllib3 = { extras = ["secure"], version = "*" }
uvicorn = { extras = ["standard"], version = "*" }
webauthn = "*"
wsproto = "*"
xmlsec = "*"
@ -176,7 +167,7 @@ bandit = "*"
black = "*"
bump2version = "*"
colorama = "*"
coverage = {extras = ["toml"],version = "*"}
coverage = { extras = ["toml"], version = "*" }
importlib-metadata = "*"
pylint = "*"
pylint-django = "*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3
info:
title: authentik
version: 2023.2.1
version: 2023.2.3
description: Making authentication simple.
contact:
email: hello@goauthentik.io

14
web/package-lock.json generated
View File

@ -22,7 +22,7 @@
"@codemirror/theme-one-dark": "^6.1.0",
"@formatjs/intl-listformat": "^7.1.7",
"@fortawesome/fontawesome-free": "^6.3.0",
"@goauthentik/api": "^2023.2.0-1676387781",
"@goauthentik/api": "^2023.2.1-1676400495",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^3.17.1",
@ -1975,9 +1975,9 @@
}
},
"node_modules/@goauthentik/api": {
"version": "2023.2.0-1676387781",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.2.0-1676387781.tgz",
"integrity": "sha512-iPNxpL6ej3irMORdQuiOmKsDqI2oc0Mrt86M+UCA6Sm16vxBjiQl8Pq9JFmQ0GLosVXQZXh7NrWM5lXJprByPA=="
"version": "2023.2.1-1676400495",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.2.1-1676400495.tgz",
"integrity": "sha512-NBP17SLYOE+AXIAyhQWdTNm6wkcruzGG1jlGaQQSGa8ShDl6g98bO7F+gLMIyOZTXeKx6+bLlKzsL+QKQ34AeA=="
},
"node_modules/@hcaptcha/types": {
"version": "1.0.3",
@ -11502,9 +11502,9 @@
"integrity": "sha512-qVtd5i1Cc7cdrqnTWqTObKQHjPWAiRwjUPaXObaeNPcy7+WKxJumGBx66rfSFgK6LNpIasVKkEgW8oyf0tmPLA=="
},
"@goauthentik/api": {
"version": "2023.2.0-1676387781",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.2.0-1676387781.tgz",
"integrity": "sha512-iPNxpL6ej3irMORdQuiOmKsDqI2oc0Mrt86M+UCA6Sm16vxBjiQl8Pq9JFmQ0GLosVXQZXh7NrWM5lXJprByPA=="
"version": "2023.2.1-1676400495",
"resolved": "https://registry.npmjs.org/@goauthentik/api/-/api-2023.2.1-1676400495.tgz",
"integrity": "sha512-NBP17SLYOE+AXIAyhQWdTNm6wkcruzGG1jlGaQQSGa8ShDl6g98bO7F+gLMIyOZTXeKx6+bLlKzsL+QKQ34AeA=="
},
"@hcaptcha/types": {
"version": "1.0.3",

View File

@ -66,7 +66,7 @@
"@codemirror/theme-one-dark": "^6.1.0",
"@formatjs/intl-listformat": "^7.1.7",
"@fortawesome/fontawesome-free": "^6.3.0",
"@goauthentik/api": "^2023.2.0-1676387781",
"@goauthentik/api": "^2023.2.1-1676400495",
"@hcaptcha/types": "^1.0.3",
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
"@lingui/cli": "^3.17.1",

View File

@ -51,7 +51,7 @@ export class GroupForm extends ModelForm<Group, string> {
patchedGroupRequest: data,
});
} else {
data.users = Array.from(this.instance?.users || []);
data.users = [];
return new CoreApi(DEFAULT_CONFIG).coreGroupsCreate({
groupRequest: data,
});

View File

@ -48,6 +48,7 @@ export class UserForm extends ModelForm<User, number> {
patchedUserRequest: data,
});
} else {
data.groups = [];
return new CoreApi(DEFAULT_CONFIG).coreUsersCreate({
userRequest: data,
});

View File

@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
export const ERROR_CLASS = "pf-m-danger";
export const PROGRESS_CLASS = "pf-m-in-progress";
export const CURRENT_CLASS = "pf-m-current";
export const VERSION = "2023.2.1";
export const VERSION = "2023.2.3";
export const TITLE_DEFAULT = "authentik";
export const ROUTE_SEPARATOR = ";";

View File

@ -29,6 +29,20 @@ export function convertToTitle(text: string): string {
});
}
/**
* Truncate a string based on maximum word count
*/
export function truncateWords(string: string, length = 10): string {
string = string || "";
const array = string.trim().split(" ");
const ellipsis = array.length > length ? "..." : "";
return array.slice(0, length).join(" ") + ellipsis;
}
/**
* Truncate a string based on character count
*/
export function truncate(string: string, length = 10): string {
return string.length > length ? `${string.substring(0, length)}...` : string;
}

View File

@ -1,6 +1,6 @@
import { uiConfig } from "@goauthentik/common/ui/config";
import { me } from "@goauthentik/common/users";
import { truncate } from "@goauthentik/common/utils";
import { truncateWords } from "@goauthentik/common/utils";
import { AKElement } from "@goauthentik/elements/Base";
import { t } from "@lingui/macro";
@ -127,7 +127,7 @@ export class LibraryApplication extends AKElement {
</div>
</div>
<div class="pf-c-card__body">
${truncate(this.application.metaDescription || "", 35)}
${truncateWords(this.application.metaDescription || "", 35)}
</div>
</div>`;
}

View File

@ -4,11 +4,30 @@ title: Terminology
slug: /terminology
---
![](/img/authentik_objects.svg)
```mermaid
graph LR
source_ldap((LDAP Source)) <-->|Synchronizes| datasource_ldap["FreeIPA/
Active Directory"]
datasource_oauth1(Twtitter) --> source_oauth((OAuth/SAML\nSource))
datasource_oauth2(GitHub) --> source_oauth((OAuth/SAML\nSource))
source_oauth --> authentik_db(authentik Database)
source_ldap --> authentik_db(authentik Database)
### System tasks
app_sso(Gitlab) --> authentik_provider[Provider]
authentik_provider --> authentik_db
authentik_provider --> authentik_app["Application
(Stores permissions and UI details)"]
authentik_app --> authentik_policy_engine[Policy Engine]
authentik_policy_engine --> authentik_db
These are longer-running tasks which authentik runs in the background. This is used to sync LDAP sources, backup the database, and other various tasks.
app_ldap("Applications that only
support LDAP (e.g. pfSense)") --> authentik_outpost_ldap[LDAP Outpost]
app_proxy("Applications that don't
support any SSO (e.g. Plex)") --> authentik_outpost_proxy[Proxy Outpost]
authentik_outpost_ldap --> authentik_outposts[Outposts]
authentik_outpost_proxy --> authentik_outposts[Outposts]
authentik_outposts --> authentik_provider
```
### Application
@ -26,7 +45,7 @@ A Provider is a way for other applications to authenticate against authentik. Co
At a base level a policy is a yes/no gate. It will either evaluate to True or False depending on the Policy Kind and settings. For example, a "Group Membership Policy" evaluates to True if the user is member of the specified Group and False if not. This can be used to conditionally apply Stages, grant/deny access to various objects, and for other custom logic.
See [Policies](./policies/)
See [Policies](../policies/index.md)
### Flows & Stages
@ -34,16 +53,20 @@ Flows are an ordered sequence of stages. These flows can be used to define how a
A stage represents a single verification or logic step. They are used to authenticate users, enroll users, and more. These stages can optionally be applied to a flow via policies.
See [Flows](./flow/)
See [Flows](../flow/index.md)
### Property Mappings
Property Mappings allow you to make information available for external applications. For example, if you want to login to AWS with authentik, you'd use Property Mappings to set the user's roles in AWS based on their group memberships in authentik.
See [Property Mappings](./property-mappings/)
See [Property Mappings](../property-mappings/index.md)
### Outpost
An outpost is a separate component of authentik, which can be deployed anywhere, regardless of the authentik deployment. The outpost offers services that aren't implemented directly into the authentik core, e.g. Reverse Proxying.
See [Outposts](./outposts/)
See [Outposts](../outposts/index.mdx)
### System tasks
These are longer-running tasks which authentik runs in the background. This is used to sync LDAP sources, backup the database, and other various tasks.

View File

@ -4,30 +4,305 @@ title: Events
Events are authentik's built-in logging system. Whenever any of the following actions occur, an event is created:
- A user logs in/logs out (including the source, if available)
- A user fails to login
- A user sets their password
- A user views a token
- An invitation is used
- A user object is written to during a flow
- A user authorizes an application
- A user links a source to their account
- A user starts/ends impersonation, including the user that was impersonated
- A policy is executed (when a policy has "Execution Logging" enabled)
- A policy or property mapping causes an exception
- A configuration error occurs, for example during the authorization of an application
- Any objects is created/updated/deleted
- An update is available
Certain information is stripped from events, to ensure no passwords or other credentials are saved in the log.
If you want to forward these events to another application, simply forward the log output of all authentik containers. Every event creation is logged there.
If you want to forward these events to another application, forward the log output of all authentik containers. Every event creation is logged with the log level "info".
### `login`
A user logs in (including the source, if available)
<details><summary>Example</summary>
<p>
```json
{
"pk": "f00f54e7-2b38-421f-bc78-e61f950048d6",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "login",
"app": "authentik.events.signals",
"context": {
"auth_method": "password",
"http_request": {
"args": {
"query": "next=%2F"
},
"path": "/api/v3/flows/executor/default-authentication-flow/",
"method": "GET"
},
"auth_method_args": {}
},
"client_ip": "::1",
"created": "2023-02-15T15:33:42.771091Z",
"expires": "2024-02-15T15:33:42.770425Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `login_failed`
A failed login attempt
<details><summary>Example</summary>
<p>
```json
{
"pk": "2779b173-eb2a-4c2b-a1a4-8283eda308d7",
"user": {
"pk": 2,
"email": "",
"username": "AnonymousUser"
},
"action": "login_failed",
"app": "authentik.events.signals",
"context": {
"stage": {
"pk": "7e88f4a991c442c1a1335d80f0827d7f",
"app": "authentik_stages_password",
"name": "default-authentication-password",
"model_name": "passwordstage"
},
"password": "********************",
"username": "akadmin",
"http_request": {
"args": {
"query": "next=%2F"
},
"path": "/api/v3/flows/executor/default-authentication-flow/",
"method": "POST"
}
},
"client_ip": "::1",
"created": "2023-02-15T15:32:55.319608Z",
"expires": "2024-02-15T15:32:55.314581Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `logout`
A user logs out.
<details><summary>Example</summary>
<p>
```json
{
"pk": "474ffb6b-77e3-401c-b681-7d618962440f",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "logout",
"app": "authentik.events.signals",
"context": {
"http_request": {
"args": {
"query": ""
},
"path": "/api/v3/flows/executor/default-invalidation-flow/",
"method": "GET"
}
},
"client_ip": "::1",
"created": "2023-02-15T15:39:55.976243Z",
"expires": "2024-02-15T15:39:55.975535Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `user_write`
A user is written to during a flow execution.
<details><summary>Example</summary>
<p>
```json
{
"pk": "d012e8af-cb94-4fa2-9e92-961e4eebc060",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "user_write",
"app": "authentik.events.signals",
"context": {
"name": "authentik Default Admin",
"email": "root@localhost",
"created": false,
"username": "akadmin",
"attributes": {
"settings": {
"locale": ""
}
},
"http_request": {
"args": {
"query": ""
},
"path": "/api/v3/flows/executor/default-user-settings-flow/",
"method": "GET"
}
},
"client_ip": "::1",
"created": "2023-02-15T15:41:18.411017Z",
"expires": "2024-02-15T15:41:18.410276Z",
"tenant": {
"pk": "fcba828076b94dedb2d5a6b4c5556fa1",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `suspicious_request`
A suspicious request has been received (for example, a revoked token was used).
### `password_set`
A user sets their password.
### `secret_view`
A user views a token's/certificate's data.
### `secret_rotate`
A token was rotated automatically by authentik.
### `invitation_used`
An invitation is used.
### `authorize_application`
A user authorizes an application.
<details><summary>Example</summary>
<p>
```json
{
"pk": "f52f9eb9-dc2a-4f1e-afea-ad5af90bf680",
"user": {
"pk": 1,
"email": "root@localhost",
"username": "akadmin"
},
"action": "authorize_application",
"app": "authentik.providers.oauth2.views.authorize",
"context": {
"geo": {
"lat": 42.0,
"city": "placeholder",
"long": 42.0,
"country": "placeholder",
"continent": "placeholder"
},
"flow": "53287faa8a644b6cb124cb602a84282f",
"scopes": "ak_proxy profile openid email",
"http_request": {
"args": {
"query": "[...]"
},
"path": "/api/v3/flows/executor/default-provider-authorization-implicit-consent/",
"method": "GET"
},
"authorized_application": {
"pk": "bed6a2495fdc4b2e8c3f93cb2ed7e021",
"app": "authentik_core",
"name": "Alertmanager",
"model_name": "application"
}
},
"client_ip": "::1",
"created": "2023-02-15T10:02:48.615499Z",
"expires": "2023-04-26T10:02:48.612809Z",
"tenant": {
"pk": "10800be643d44842ab9d97cb5f898ce9",
"app": "authentik_tenants",
"name": "Default tenant",
"model_name": "tenant"
}
}
```
</p>
</details>
### `source_linked`
A user links a source to their account
### `impersonation_started` / `impersonation_ended`
A user starts/ends impersonation, including the user that was impersonated
### `policy_execution`
A policy is executed (when a policy has "Execution Logging" enabled).
### `policy_exception` / `property_mapping_exception`
A policy or property mapping causes an exception
### `system_task_exception`
An exception occurred in a system task.
### `system_exception`
A general exception in authentik occurred.
### `configuration_error`
A configuration error occurs, for example during the authorization of an application
### `model_created` / `model_updated` / `model_deleted`
Logged when any model is created/updated/deleted, including the user that sent the request.
### `email_sent`
An email has been sent. Included is the email that was sent.
### `update_available`
An update is available

View File

@ -99,7 +99,7 @@ This includes the following:
- `context['application']`: The application the user is in the process of authorizing. (Optional)
- `context['source']`: The source the user is authenticating/enrolling with. (Optional)
- `context['pending_user']`: The currently pending user, see [User](../user-group/user.md#object-attributes)
- `context['is_restored']`: Set to `True` when the flow plan has been restored from a flow token, for example the user clicked a link to a flow which was sent by an email stage. (Optional)
- `context['is_restored']`: Contains the flow token when the flow plan was restored from a link, for example the user clicked a link to a flow which was sent by an email stage. (Optional)
- `context['auth_method']`: Authentication method (this value is set by password stages) (Optional)
Depending on method, `context['auth_method_args']` is also set.

View File

@ -1,28 +1,94 @@
---
title: Overview
title: Proxy Provider
---
```mermaid
sequenceDiagram
participant u as User accesses service
participant rp as Reverse proxy
participant ak as authentik
participant s as Service
u->>rp: Initial request
rp->>ak: Checks authentication
alt User is authenticated
ak ->> rp: Successful response
rp ->> s: Initial request is forwarded
else User needs to be authenticated
ak ->> rp: Redirect to the login page
rp ->> u: Redirect is passed to enduser
end
```
## Headers
The proxy outpost sets the following user-specific headers:
- X-authentik-username: `akadmin`
### `X-authentik-username`
The username of the currently logged in user
Example value: `akadmin`
- X-authentik-groups: `foo|bar|baz`
The username of the currently logged in user
The groups the user is member of, separated by a pipe
### `X-authentik-groups`
- X-authentik-email: `root@localhost`
Example value: `foo|bar|baz`
The email address of the currently logged in user
The groups the user is member of, separated by a pipe
- X-authentik-name: `authentik Default Admin`
### `X-authentik-email`
Full name of the current user
Example value: `root@localhost`
- X-authentik-uid: `900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb`
The email address of the currently logged in user
The hashed identifier of the currently logged in user.
### `X-authentik-name`
Example value: `authentik Default Admin`
Full name of the current user
### `X-authentik-uid`
Example value: `900347b8a29876b45ca6f75722635ecfedf0e931c6022e3a29a8aa13fb5516fb`
The hashed identifier of the currently logged in user.
Besides these user-specific headers, some application specific headers are also set:
### `X-authentik-meta-outpost`
Example value: `authentik Embedded Outpost`
The authentik outpost's name.
### `X-authentik-meta-provider`
Example value: `test`
The authentik provider's name.
### `X-authentik-meta-app`
Example value: `test`
The authentik application's slug.
### `X-authentik-meta-version`
Example value: `goauthentik.io/outpost/1.2.3`
The authentik outpost's version.
### `X-Forwarded-Host`
:::info
Only set in proxy mode
:::
The original Host header sent by the client. This is set as the `Host` header is set to the host of the configured backend.
### Additional headers
Additionally, you can set `additionalHeaders` attribute on groups or users to set additional headers:
@ -31,30 +97,6 @@ additionalHeaders:
X-test-header: test-value
```
Besides these user-specific headers, some application specific headers are also set:
- X-authentik-meta-outpost: `authentik Embedded Outpost`
The authentik outpost's name.
- X-authentik-meta-provider: `test`
The authentik provider's name.
- X-authentik-meta-app: `test`
The authentik application's slug.
- X-authentik-meta-version: `goauthentik.io/outpost/1.2.3`
The authentik outpost's version.
### Only in proxy mode
- X-Forwarded-Host:
The original Host header sent by the client. This is set as the `Host` header is set to the host of the configured backend.
## HTTPS
The outpost listens on both 9000 for HTTP and 9443 for HTTPS.

View File

@ -0,0 +1,27 @@
# CVE-2023-26481
_Reported by [@fuomag9](https://github.com/fuomag9)_
## Insufficient user check in FlowTokens by Email stage
### Summary
Due to an insufficient access check, a recovery flow link that is created by an admin (or sent via email by an admin) can be used to set the password for any arbitrary user.
### Patches
authentik 2022.12.3, 2023.1.3, 2023.2.3 fix this issue.
### Impact
This attack is only possible if a recovery flow exists, which has both an Identification and an Email stage bound to it. If the flow has policies on the identification stage to skip it when the flow is restored (by checking `request.context['is_restored']`), the flow is not affected by this. With this flow in place, an administrator must create a recovery Link or send a recovery URL to the attacker, who can, due to the improper validation of the token create, set the password for any account.
### Workaround
It is recommended to upgrade to the patched version of authentik. Regardless, for custom recovery flows it is recommended to add a policy that checks if the flow is restored, and skips the identification stage.
### For more information
If you have any questions or comments about this advisory:
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)

View File

@ -48,15 +48,15 @@ module.exports = {
},
{
type: "dropdown",
label: `Version: latest`,
label: `Version: ${releases[0].replace(
/releases\/\d+\/v/,
""
)}`,
position: "right",
items: releases.map((release) => {
const subdomain = release
.replace(/releases\/\d+\/v/, "")
.replace(".", "-");
const label =
"Version: " +
release.replace(/releases\/\d+\/v/, "");
const version = release.replace(/releases\/\d+\/v/, "");
const subdomain = version.replace(".", "-");
const label = `Version: ${version}`;
return {
label: label,
href: `https://version-${subdomain}.goauthentik.io`,
@ -169,6 +169,10 @@ module.exports = {
},
],
],
markdown: {
mermaid: true,
},
themes: ["@docusaurus/theme-mermaid"],
scripts: [
{
src: "https://goauthentik.io/js/script.js",

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@
"dependencies": {
"@docusaurus/plugin-client-redirects": "2.3.1",
"@docusaurus/preset-classic": "2.3.1",
"@docusaurus/theme-mermaid": "^2.3.1",
"@mdx-js/react": "^1.6.22",
"clsx": "^1.2.1",
"disqus-react": "^1.1.5",

View File

@ -300,9 +300,10 @@ module.exports = {
},
items: [
"security/policy",
"security/CVE-2022-23555",
"security/CVE-2022-46145",
"security/CVE-2022-46172",
"security/CVE-2022-23555",
"security/CVE-2023-26481",
],
},
],

View File

@ -24,6 +24,7 @@
}
.hero--primary {
padding-bottom: 5.3rem !important;
-webkit-clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 3vw));
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 calc(100% - 3vw));
}

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 24 KiB