Compare commits

...

191 Commits

Author SHA1 Message Date
8f995aab62 web: break application view into constituent parts
As part of the project to make the verticals more controllable and responsive, this commit breaks
the ApplicationView into two different parts: The API layer and the rendering layer.

The rendering layer is officially dumb beyond words; it knows nothing at all about Applications,
RBAC, or Outposts; it just draws what it's told to draw. It has parts inside that have their own
reactivity, but that reactivity means nothing to the renderer.

The Renderer itself is broken into two: The LoadingRenderer works when there is no application, and
the regular Renderer is build when there is.  Typescript's check makes it impossible to attempt to
use the standard renderer when there is no application, so all of the `this.application?` checks
just... go away.

A _huge_ section of the View is the control card, which offers the user the power to visit the
provider, provide access to the backchannel providers, edit the application, run an access check
against a given user, and launch the application. All of these features were heavily obscured by a
blizzard of dg/dl/dt/dd html objects that made it hard to see what was in there. Each "description"
pair has been broken out into a tuple of Term and Description, with filters to remove the ones that
aren't applicable whenever an application doesn't have, for example, a launch url, or backchannel
providers, and a utility function I wrote _ages_ ago renders the description list syntax for me
without my having to do it all by hand.

The nice thing about this work is that it now allows me to *see* where in the ApplicationView code
to focus my efforts on providing activation hooks for the "create a new policy," "assign a new
permission to a user," or "edit an application" commands that should be accessible by the palette,
or even from the sidebar.

The other nice thing is that it reveals just *where* in our code to focus our efforts on revamping
our styling, and making it better for ourselves and our users.

There's a reason I call this my *legibility project*.

It didn't even take that long... about 2½ hours, and I'm only going to get faster at it as the needs
of the different components become clear.
2024-05-08 17:47:26 -07:00
2846e49657 Added comments to explain something. 2024-05-08 13:56:46 -07:00
0e60e755d4 Eslint had opinions. 2024-05-08 13:50:47 -07:00
6cf2433e2b web: fix value handling inside controlled components
This is one of those stupid bugs that drive web developers crazy. The basics are straightforward:
when you cause a higher-level component to have a "big enough re-render," for some unknown
definition of "big enough," it will re-render the sub-components. In traditional web interaction,
those components should never be re-rendered while the user is interacting with the form, but in
frameworks where there's dynamic re-arrangement, part or all of the form could get re-rendered at
any mmoment. Since neither the form nor any of its intermediaries is tracking the values as they're
changed, it's up to the components themselves to keep the user's input-- and to be hardened against
property changes coming from the outside world.

So static memoization of the initial value passed in, and aggressively walling off the values the
customer generates from that field, are needed to protect the user's work from any framework's
dynamic DOM management. I remember struggling with this in React; I had hoped Lit was better, but in
this case, not better enough.

The protocol for "is it an ak-data-control" is "it has a `json()` method that returns the data ready
to be sent to the authentik server."  I missed that in one place, so that's on me.
2024-05-08 13:38:49 -07:00
e1d565d40e Merge branch 'main' into dev
* main:
  core, web: update translations (#9633)
  core: bump google-api-python-client from 2.127.0 to 2.128.0 (#9641)
  core: bump goauthentik.io/api/v3 from 3.2024041.3 to 3.2024042.2 (#9635)
  core: bump golang from 1.22.2-bookworm to 1.22.3-bookworm (#9636)
  web: bump the esbuild group in /web with 2 updates (#9637)
  web: bump esbuild from 0.21.0 to 0.21.1 in /web (#9639)
  core: bump django from 5.0.5 to 5.0.6 (#9640)
  web: bump API Client version (#9630)
  enterprise/providers/google: initial account sync to google workspace (#9384)
  web/flows: fix error when using consecutive webauthn validator stages (#9629)
  web: bump API Client version (#9626)
  website/docs: refine intro page for sources (#9625)
  release: 2024.4.2
2024-05-08 08:06:05 -07:00
04d613d213 core, web: update translations (#9633)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-05-08 15:12:14 +02:00
b5928c2f7f core: bump google-api-python-client from 2.127.0 to 2.128.0 (#9641)
Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.127.0 to 2.128.0.
- [Release notes](https://github.com/googleapis/google-api-python-client/releases)
- [Changelog](https://github.com/googleapis/google-api-python-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/googleapis/google-api-python-client/compare/v2.127.0...v2.128.0)

---
updated-dependencies:
- dependency-name: google-api-python-client
  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>
2024-05-08 11:58:09 +02:00
c8e7247d2c core: bump goauthentik.io/api/v3 from 3.2024041.3 to 3.2024042.2 (#9635)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024041.3 to 3.2024042.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024041.3...v3.2024042.2)

---
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>
2024-05-08 11:57:39 +02:00
ac6266a23a core: bump golang from 1.22.2-bookworm to 1.22.3-bookworm (#9636)
Bumps golang from 1.22.2-bookworm to 1.22.3-bookworm.

---
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>
2024-05-08 11:57:16 +02:00
88213f67ee web: bump the esbuild group in /web with 2 updates (#9637)
Bumps the esbuild group in /web with 2 updates: [@esbuild/darwin-arm64](https://github.com/evanw/esbuild) and [@esbuild/linux-arm64](https://github.com/evanw/esbuild).


Updates `@esbuild/darwin-arm64` from 0.21.0 to 0.21.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.0...v0.21.1)

Updates `@esbuild/linux-arm64` from 0.21.0 to 0.21.1
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.0...v0.21.1)

---
updated-dependencies:
- dependency-name: "@esbuild/darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: esbuild
- dependency-name: "@esbuild/linux-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: esbuild
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-08 11:57:06 +02:00
f8fd17f77e web: bump esbuild from 0.21.0 to 0.21.1 in /web (#9639)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.21.0 to 0.21.1.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.21.0...v0.21.1)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  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>
2024-05-08 11:56:54 +02:00
7f127ee515 core: bump django from 5.0.5 to 5.0.6 (#9640)
Bumps [django](https://github.com/django/django) from 5.0.5 to 5.0.6.
- [Commits](https://github.com/django/django/compare/5.0.5...5.0.6)

---
updated-dependencies:
- dependency-name: django
  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>
2024-05-08 11:56:42 +02:00
ed214b4ac8 web: bump API Client version (#9630)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-05-07 18:12:43 +00:00
aeb1b450eb enterprise/providers/google: initial account sync to google workspace (#9384)
* providers/google: initial account sync to google workspace

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start separating scim sync client

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* generalize more...ish

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set dispatch_uid

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start generalizing task

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fully separate tasks

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix more

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix signals...?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start google dedupe

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* drawing the rest of the owl

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* more

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* juse use a whole lot less magic

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* member sync, better implement conflict/retry-able exceptions

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* max wizards taller

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* gen api, basic UI

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix some bugs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix a bunch more bugs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* generalize sync status API

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rework sync chart

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add slugify to evaluator

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add test property mappings

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* rename to google workspace

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* handle existing objects

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix credential render

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* verify email has correct domain before syncing user

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing docstring

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix lock not being used

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* abstract more common stuff away

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* backport time limit fix

https://github.com/goauthentik/authentik/pull/9546
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start discovery

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* implement discover for google

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* prevent same issue as with https://github.com/goauthentik/authentik/pull/9557

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix sync status

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make group name unique in API

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix reference to old wrapper

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start adding tests

man this api client is awful

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add SkipObject

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* dont use weak ref

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add group tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add user and group delete options

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set user agent

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* if the api's testing tools are awful, let's just make our own

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add more tests and already fix some more bugs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add discover

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add preview banner

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add group import test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only import users/groups in the correct parent group

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix conflicting args

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing schedule

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix web ui

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add default_group_email_domain

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-07 19:52:20 +02:00
18b4b2d7b2 web/flows: fix error when using consecutive webauthn validator stages (#9629)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-07 19:50:57 +02:00
a140bad8fb web: bump API Client version (#9626)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-05-07 17:05:03 +00:00
bb1b8ab7bb website/docs: refine intro page for sources (#9625) 2024-05-07 18:59:25 +02:00
6802614fbf release: 2024.4.2 2024-05-07 18:45:37 +02:00
ee37e9235b Merge branch 'main' into dev
* main: (42 commits)
  website/docs: prepare 2024.4.2 release notes (#9555)
  web: bump the esbuild group in /web with 2 updates (#9616)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9611)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9560)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9563)
  ci: bump golangci/golangci-lint-action from 5 to 6 (#9615)
  web: bump esbuild from 0.20.2 to 0.21.0 in /web (#9617)
  core: bump cryptography from 42.0.6 to 42.0.7 (#9618)
  core: bump sentry-sdk from 2.0.1 to 2.1.1 (#9619)
  core: bump django from 5.0.4 to 5.0.5 (#9620)
  core, web: update translations (#9613)
  website/docs: move Sources from Integrations into Docs (#9515)
  website/docs: add procedurals to flow inspector docs (#9556)
  core: bump jinja2 from 3.1.3 to 3.1.4 (#9610)
  web: clean up the options rendering in PromptForm (#9564)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9608)
  website/docs: add instructions for deploying radius manually with docker compose (#9605)
  sources/scim: fix duplicate groups and invalid schema (#9466)
  core: fix condition in task clean_expiring_models (#9603)
  translate: Updates for file web/xliff/en.xlf in fr (#9600)
  ...
2024-05-07 08:49:15 -07:00
619113e810 website/docs: prepare 2024.4.2 release notes (#9555)
Co-authored-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
2024-05-07 16:22:24 +02:00
a8697bf1ad web: bump the esbuild group in /web with 2 updates (#9616)
Bumps the esbuild group in /web with 2 updates: [@esbuild/darwin-arm64](https://github.com/evanw/esbuild) and [@esbuild/linux-arm64](https://github.com/evanw/esbuild).


Updates `@esbuild/darwin-arm64` from 0.20.2 to 0.21.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.20.2...v0.21.0)

Updates `@esbuild/linux-arm64` from 0.20.2 to 0.21.0
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.20.2...v0.21.0)

---
updated-dependencies:
- dependency-name: "@esbuild/darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: esbuild
- dependency-name: "@esbuild/linux-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: esbuild
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 12:27:32 +02:00
f52dec4b7e translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9611)
* Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

* Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

* Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

* Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

* Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-07 12:21:07 +02:00
6560bf18a4 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9560)
Translate locale/en/LC_MESSAGES/django.po in zh_CN

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-07 12:20:51 +02:00
315cd40e6a translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9563)
Translate django.po in zh-Hans

100% translated source file: 'django.po'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-07 12:20:40 +02:00
a7a62b5005 ci: bump golangci/golangci-lint-action from 5 to 6 (#9615)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 5 to 6.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 12:18:41 +02:00
37e3998211 web: bump esbuild from 0.20.2 to 0.21.0 in /web (#9617)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.20.2 to 0.21.0.
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.20.2...v0.21.0)

---
updated-dependencies:
- dependency-name: esbuild
  dependency-type: direct:development
  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>
2024-05-07 12:18:31 +02:00
31be26ebbd core: bump cryptography from 42.0.6 to 42.0.7 (#9618)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.6 to 42.0.7.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.6...42.0.7)

---
updated-dependencies:
- dependency-name: cryptography
  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>
2024-05-07 12:17:39 +02:00
42b1cb06fb core: bump sentry-sdk from 2.0.1 to 2.1.1 (#9619)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.0.1 to 2.1.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.0.1...2.1.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  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>
2024-05-07 12:17:26 +02:00
066ec35adf core: bump django from 5.0.4 to 5.0.5 (#9620)
Bumps [django](https://github.com/django/django) from 5.0.4 to 5.0.5.
- [Commits](https://github.com/django/django/compare/5.0.4...5.0.5)

---
updated-dependencies:
- dependency-name: django
  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>
2024-05-07 12:17:06 +02:00
87a808a747 core, web: update translations (#9613)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-05-07 02:11:50 +02:00
d8b1cd757e website/docs: move Sources from Integrations into Docs (#9515)
* moved Sources form Integrations to Docs

* files moved

* fixed setting options

* fixed broken links and added redirects

* try single-sourcing Sources docs

* tweaks

* fighting links

* still fighting links

* fightng sidebar

* fighting with sidebar

* add logos and tweak

* image tweaks

* Optimised images with calibre/image-actions

* added remaining UI definitions

* kens edits

---------

Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-05-06 17:49:17 -05:00
b1b9c8e0e5 website/docs: add procedurals to flow inspector docs (#9556)
* clarify access process

* add image

* link to permissions and explain query parm

* typos

* changed image

* edits

---------

Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-05-06 14:59:01 -05:00
a0a617055b core: bump jinja2 from 3.1.3 to 3.1.4 (#9610)
Bumps [jinja2](https://github.com/pallets/jinja) from 3.1.3 to 3.1.4.
- [Release notes](https://github.com/pallets/jinja/releases)
- [Changelog](https://github.com/pallets/jinja/blob/main/CHANGES.rst)
- [Commits](https://github.com/pallets/jinja/compare/3.1.3...3.1.4)

---
updated-dependencies:
- dependency-name: jinja2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 18:22:52 +02:00
9ec6f548a6 web: clean up the options rendering in PromptForm (#9564)
* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: clean up a bit inside the promptform

Just something I did while researching Github Issue 5197.
2024-05-06 08:55:43 -07:00
46980db582 translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9608)
Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 17:03:09 +02:00
d8fd1ddec6 website/docs: add instructions for deploying radius manually with docker compose (#9605) 2024-05-06 15:38:48 +02:00
74d29e2374 sources/scim: fix duplicate groups and invalid schema (#9466)
* sources/scim: fix duplicate groups

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing schema in response

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix members missing in returned group

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* optimise queries

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-06 14:55:10 +02:00
801a28ef65 core: fix condition in task clean_expiring_models (#9603) 2024-05-06 12:29:04 +00:00
3fff090612 translate: Updates for file web/xliff/en.xlf in fr (#9600)
Translate web/xliff/en.xlf in fr

100% translated source file: 'web/xliff/en.xlf'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 12:14:22 +00:00
b071d55b4d translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#9599)
Translate locale/en/LC_MESSAGES/django.po in fr

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'fr'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 12:12:57 +00:00
244cbc5b6d core: fix task clean_expiring_models removing valid sessions with using database storage (#9598) 2024-05-06 12:02:03 +00:00
74da359dd5 translate: Updates for file web/xliff/en.xlf in zh-Hans (#9587)
Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 12:40:07 +02:00
56b73e3bd5 translate: Updates for file web/xliff/en.xlf in zh_CN (#9588)
Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 12:39:51 +02:00
59e3c85568 core: bump golang.org/x/oauth2 from 0.19.0 to 0.20.0 (#9590)
Bumps [golang.org/x/oauth2](https://github.com/golang/oauth2) from 0.19.0 to 0.20.0.
- [Commits](https://github.com/golang/oauth2/compare/v0.19.0...v0.20.0)

---
updated-dependencies:
- dependency-name: golang.org/x/oauth2
  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>
2024-05-06 12:38:22 +02:00
746c933e63 core: bump goauthentik.io/api/v3 from 3.2024041.2 to 3.2024041.3 (#9589)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024041.2 to 3.2024041.3.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024041.2...v3.2024041.3)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  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>
2024-05-06 12:38:15 +02:00
f165bbca5d web: bump the storybook group in /web with 7 updates (#9591)
Bumps the storybook group in /web with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/essentials) | `8.0.9` | `8.0.10` |
| [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links) | `8.0.9` | `8.0.10` |
| [@storybook/blocks](https://github.com/storybookjs/storybook/tree/HEAD/code/ui/blocks) | `8.0.9` | `8.0.10` |
| [@storybook/manager-api](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/manager-api) | `8.0.9` | `8.0.10` |
| [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) | `8.0.9` | `8.0.10` |
| [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite) | `8.0.9` | `8.0.10` |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/cli) | `8.0.9` | `8.0.10` |


Updates `@storybook/addon-essentials` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/addons/essentials)

Updates `@storybook/addon-links` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/addons/links)

Updates `@storybook/blocks` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/ui/blocks)

Updates `@storybook/manager-api` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/lib/manager-api)

Updates `@storybook/web-components` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/frameworks/web-components-vite)

Updates `storybook` from 8.0.9 to 8.0.10
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.10/code/lib/cli)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/blocks"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/manager-api"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-06 12:38:06 +02:00
f335b08ec2 core: bump coverage from 7.5.0 to 7.5.1 (#9594)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.5.0 to 7.5.1.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.5.0...7.5.1)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:development
  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>
2024-05-06 12:36:03 +02:00
6e831a4253 core: bump django-storages from 1.14.2 to 1.14.3 (#9595)
Bumps [django-storages](https://github.com/jschneier/django-storages) from 1.14.2 to 1.14.3.
- [Changelog](https://github.com/jschneier/django-storages/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/jschneier/django-storages/compare/1.14.2...1.14.3)

---
updated-dependencies:
- dependency-name: django-storages
  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>
2024-05-06 12:35:36 +02:00
6c1687c569 core: bump cryptography from 42.0.5 to 42.0.6 (#9596)
Bumps [cryptography](https://github.com/pyca/cryptography) from 42.0.5 to 42.0.6.
- [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pyca/cryptography/compare/42.0.5...42.0.6)

---
updated-dependencies:
- dependency-name: cryptography
  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>
2024-05-06 12:33:26 +02:00
09c64e2354 core: bump ruff from 0.4.2 to 0.4.3 (#9597)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.2 to 0.4.3.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.2...v0.4.3)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  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>
2024-05-06 12:33:13 +02:00
0a312821ee website/docs: fix api browser references (#9577)
* website/docs: fix api browser references

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* don't attempt to correct generated docs

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix?

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-06 12:30:43 +02:00
06d1062423 tenants: fix scheduled tasks not running on default tenant (#9583)
* tenants: fix scheduled tasks not running on default tenant

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add some extra time to keep system task around

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* make sure we actually send it to all tenants

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-06 03:16:30 +02:00
dcfa3dc88a web: bump API Client version (#9585)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-05-06 03:10:39 +02:00
c45bb8e985 providers/proxy: rework redirect mechanism (#8594)
* providers/proxy: rework redirect mechanism

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add session id, don't tie to state in session

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* handle state failing to parse

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* save session after creating state

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* remove debug

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* include task expiry in status

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix redirect URL detection

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-06 03:07:08 +02:00
3e4fea875a translate: Updates for file web/xliff/en.xlf in zh-Hans (#9562)
* Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

* Removing web/xliff/en.xlf in zh-Hans

99% of minimum 100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

---------

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 02:54:38 +02:00
c7670d271a translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9580)
Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-05-06 02:23:21 +02:00
570f3a4d42 core, web: update translations (#9582)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-05-06 02:23:06 +02:00
3c54e94c6e providers/scim: fix SCIM ID incorrectly used as primary key (#9557)
* providers/scim: fix SCIM ID incorrectly used as primary key

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix unique together

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add test

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add check for empty scim ID

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-05 17:32:19 +02:00
26daaeb57d core: fix source_flow_manager saving user-source connection too early (#9559)
* core: fix source_flow_manager saving user-source connection too early

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* ci: fix branch name

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-05 16:15:21 +02:00
a60442fc2c enterprise/audit: fix audit logging with m2m relations (#9571) 2024-05-05 02:33:38 +02:00
8790f7059a website/docs: switch API browser renderer to PAN (#9570)
* website/docs: switch API browser renderer to PAN

https://github.com/PaloAltoNetworks/docusaurus-openapi-docs/
Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* hey we can actually directly link to API endpoints now

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* set as sub category

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* revert sidebars back to JS for tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-04 23:49:33 +02:00
8248163958 Merge branch 'main' into dev
* main:
  website/docs: fix openssl rand commands (#9554)
  web: bump @sentry/browser from 7.112.2 to 7.113.0 in /web in the sentry group (#9549)
  core, web: update translations (#9548)
  core: bump goauthentik.io/api/v3 from 3.2024041.1 to 3.2024041.2 (#9551)
  core: bump django-model-utils from 4.5.0 to 4.5.1 (#9550)
  providers/scim: fix time_limit not set correctly (#9546)
2024-05-03 13:40:13 -07:00
9acebec1f6 Merge branch 'main' into dev
* main:
  web/flows: fix error when enrolling multiple WebAuthn devices consecutively (#9545)
  web: bump ejs from 3.1.9 to 3.1.10 in /tests/wdio (#9542)
  web: bump API Client version (#9543)
  providers/saml: fix ecdsa support (#9537)
  website/integrations: nextcloud: connect to existing user (#9155)
2024-05-03 08:22:55 -07:00
49cf10e9bd website/docs: fix openssl rand commands (#9554)
* website/docs: fix openssl rand commands

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update website/integrations/sources/freeipa/index.md

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: Jens L. <jens@beryju.org>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-05-03 14:24:04 +02:00
13da6f5151 web: bump @sentry/browser from 7.112.2 to 7.113.0 in /web in the sentry group (#9549)
web: bump @sentry/browser in /web in the sentry group

Bumps the sentry group in /web with 1 update: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 7.112.2 to 7.113.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.113.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.112.2...7.113.0)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-03 13:22:21 +02:00
a1e0564f8f core, web: update translations (#9548)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-05-03 13:22:10 +02:00
55f3664063 core: bump goauthentik.io/api/v3 from 3.2024041.1 to 3.2024041.2 (#9551)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024041.1 to 3.2024041.2.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024041.1...v3.2024041.2)

---
updated-dependencies:
- dependency-name: goauthentik.io/api/v3
  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>
2024-05-03 13:20:24 +02:00
baabd8614f core: bump django-model-utils from 4.5.0 to 4.5.1 (#9550)
Bumps [django-model-utils](https://github.com/jazzband/django-model-utils) from 4.5.0 to 4.5.1.
- [Release notes](https://github.com/jazzband/django-model-utils/releases)
- [Changelog](https://github.com/jazzband/django-model-utils/blob/4.5.1/CHANGES.rst)
- [Commits](https://github.com/jazzband/django-model-utils/compare/4.5.0...4.5.1)

---
updated-dependencies:
- dependency-name: django-model-utils
  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>
2024-05-03 13:20:14 +02:00
79df24f4eb providers/scim: fix time_limit not set correctly (#9546)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-03 11:48:34 +02:00
f1afc4d263 web/flows: fix error when enrolling multiple WebAuthn devices consecutively (#9545)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-02 19:54:21 +02:00
643a256f01 web: bump ejs from 3.1.9 to 3.1.10 in /tests/wdio (#9542)
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-02 15:41:04 +02:00
b7f92ef0ea web: bump API Client version (#9543)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-05-02 13:37:44 +00:00
e33ca93f05 providers/saml: fix ecdsa support (#9537)
* crypto: add option to select which alg to use to generate

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix missing ecdsa options for XML signing

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* bump xml libraries and remove disclaimer

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* lock djangoframework

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-02 15:18:14 +02:00
79af8b8638 website/integrations: nextcloud: connect to existing user (#9155)
* doc: integration: nextcloud: connect to existing user

Add instruction on how to link an existing Nextcloud user to the authentik user.

Signed-off-by: Pierrick Guillaume <34305318+Fymyte@users.noreply.github.com>

* Apply suggested changes

Signed-off-by: Pierrick Guillaume <pierguill@gmail.com>

* Fix missing letter

Signed-off-by: Pierrick Guillaume <pierguill@gmail.com>

* Run prettier

* Remove tip

* fix federated cloud id tip and indentation

---------

Signed-off-by: Pierrick Guillaume <34305318+Fymyte@users.noreply.github.com>
Signed-off-by: Pierrick Guillaume <pierguill@gmail.com>
Co-authored-by: Pierrick Guillaume <pguillaume@sequans.com>
2024-05-02 07:23:07 -05:00
2a96900dc7 Merge branch 'main' into dev
* main: (43 commits)
  stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#9535)
  web: bump the rollup group across 1 directory with 3 updates (#9532)
  website/developer-docs: Add note for custom YAML tags in an IDE (#9528)
  lifecycle: close database connection after migrating (#9516)
  web: bump the babel group in /web with 3 updates (#9520)
  core: bump node from 21 to 22 (#9521)
  web: bump @codemirror/lang-python from 6.1.5 to 6.1.6 in /web (#9523)
  providers/rac: bump guacd to 1.5.5 (#9514)
  core: only prefetch related objects when required (#9476)
  website/integrations: move Fortimanager to Networking (#9505)
  website: bump react-tooltip from 5.26.3 to 5.26.4 in /website (#9494)
  web: bump the rollup group in /web with 3 updates (#9497)
  web: bump yaml from 2.4.1 to 2.4.2 in /web (#9499)
  core: bump goauthentik.io/api/v3 from 3.2024040.1 to 3.2024041.1 (#9503)
  core: bump pytest from 8.1.1 to 8.2.0 (#9501)
  website: bump react-dom from 18.3.0 to 18.3.1 in /website (#9495)
  website: bump react and @types/react in /website (#9496)
  web: bump react-dom from 18.3.0 to 18.3.1 in /web (#9498)
  core: bump sentry-sdk from 2.0.0 to 2.0.1 (#9502)
  web/flows: fix missing fallback for flow logo (#9487)
  ...
2024-05-01 17:24:34 -07:00
d2b8bd3635 stages/authenticator_webauthn: Update FIDO MDS3 & Passkey aaguid blobs (#9535)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-05-01 12:38:06 +02:00
02e01559f4 web: bump the rollup group across 1 directory with 3 updates (#9532)
Bumps the rollup group with 3 updates in the /web directory: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) and [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.17.0 to 4.17.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.17.0...v4.17.2)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.17.0 to 4.17.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.17.0...v4.17.2)

Updates `@rollup/rollup-linux-x64-gnu` from 4.17.0 to 4.17.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.17.0...v4.17.2)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 22:14:40 +02:00
b0c39e4843 website/developer-docs: Add note for custom YAML tags in an IDE (#9528)
Add note for custom tags in an IDE

Custom tags are not provided via the schema file, but must be defined in the IDE. If this is not done, the IDE displays syntax errors when using the custom tags.

Signed-off-by: Nils Mittler <70568139+mittler-works@users.noreply.github.com>
2024-04-30 15:08:30 -05:00
039570a140 lifecycle: close database connection after migrating (#9516)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-30 21:04:30 +02:00
fdc7dedc58 web: bump the babel group in /web with 3 updates (#9520)
Bumps the babel group in /web with 3 updates: [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core), [@babel/plugin-transform-private-property-in-object](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-private-property-in-object) and [@babel/preset-env](https://github.com/babel/babel/tree/HEAD/packages/babel-preset-env).


Updates `@babel/core` from 7.24.4 to 7.24.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.5/packages/babel-core)

Updates `@babel/plugin-transform-private-property-in-object` from 7.24.1 to 7.24.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.5/packages/babel-plugin-transform-private-property-in-object)

Updates `@babel/preset-env` from 7.24.4 to 7.24.5
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.24.5/packages/babel-preset-env)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/plugin-transform-private-property-in-object"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
- dependency-name: "@babel/preset-env"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: babel
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 21:04:05 +02:00
098fcdeaf2 core: bump node from 21 to 22 (#9521)
Bumps node from 21 to 22.

---
updated-dependencies:
- dependency-name: node
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-30 21:03:57 +02:00
3cf9278bea web: bump @codemirror/lang-python from 6.1.5 to 6.1.6 in /web (#9523)
Bumps [@codemirror/lang-python](https://github.com/codemirror/lang-python) from 6.1.5 to 6.1.6.
- [Changelog](https://github.com/codemirror/lang-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codemirror/lang-python/compare/6.1.5...6.1.6)

---
updated-dependencies:
- dependency-name: "@codemirror/lang-python"
  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>
2024-04-30 21:03:46 +02:00
13ccb352d7 providers/rac: bump guacd to 1.5.5 (#9514)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 19:49:03 +02:00
c5b099856d core: only prefetch related objects when required (#9476)
* core: only prefetch related objects when required

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add tests to assert query count

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* "optimize" another query away

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* prefetch parent and roles

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* whops that needs to be pre-fetched

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-29 12:59:59 +02:00
6d912be7f6 website/integrations: move Fortimanager to Networking (#9505)
move Fortimanager to Networking

Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-04-29 05:20:54 -05:00
0c54d266d3 website: bump react-tooltip from 5.26.3 to 5.26.4 in /website (#9494)
Bumps [react-tooltip](https://github.com/ReactTooltip/react-tooltip) from 5.26.3 to 5.26.4.
- [Release notes](https://github.com/ReactTooltip/react-tooltip/releases)
- [Changelog](https://github.com/ReactTooltip/react-tooltip/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ReactTooltip/react-tooltip/compare/v5.26.3...v5.26.4)

---
updated-dependencies:
- dependency-name: react-tooltip
  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>
2024-04-29 11:37:31 +02:00
c4784cf383 web: bump the rollup group in /web with 3 updates (#9497)
Bumps the rollup group in /web with 3 updates: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) and [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.16.4 to 4.17.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.4...v4.17.0)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.16.4 to 4.17.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.4...v4.17.0)

Updates `@rollup/rollup-linux-x64-gnu` from 4.16.4 to 4.17.0
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.4...v4.17.0)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-29 11:19:08 +02:00
44ccbe2fdf web: bump yaml from 2.4.1 to 2.4.2 in /web (#9499)
Bumps [yaml](https://github.com/eemeli/yaml) from 2.4.1 to 2.4.2.
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.4.1...v2.4.2)

---
updated-dependencies:
- dependency-name: yaml
  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>
2024-04-29 11:18:57 +02:00
d2615f0d6a core: bump goauthentik.io/api/v3 from 3.2024040.1 to 3.2024041.1 (#9503)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024040.1 to 3.2024041.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024040.1...v3.2024041.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>
2024-04-29 11:18:39 +02:00
5ab3cf4952 core: bump pytest from 8.1.1 to 8.2.0 (#9501)
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.1.1 to 8.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.1.1...8.2.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:development
  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>
2024-04-29 11:18:30 +02:00
1926a472cd website: bump react-dom from 18.3.0 to 18.3.1 in /website (#9495)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 18.3.0 to 18.3.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  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>
2024-04-29 11:18:18 +02:00
d220ca6bab website: bump react and @types/react in /website (#9496)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

Updates `react` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react)

Updates `@types/react` from 18.3.0 to 18.3.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react
  dependency-type: direct:production
  update-type: version-update:semver-patch
- dependency-name: "@types/react"
  dependency-type: direct:development
  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>
2024-04-29 11:18:01 +02:00
759ea731bf web: bump react-dom from 18.3.0 to 18.3.1 in /web (#9498)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 18.3.0 to 18.3.1.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v18.3.1/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-type: direct:development
  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>
2024-04-29 11:17:43 +02:00
e01fd5eb1a core: bump sentry-sdk from 2.0.0 to 2.0.1 (#9502)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.0.0 to 2.0.1.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/2.0.0...2.0.1)

---
updated-dependencies:
- dependency-name: sentry-sdk
  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>
2024-04-29 11:17:28 +02:00
e716e24ec6 web/flows: fix missing fallback for flow logo (#9487)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-28 16:35:19 +02:00
e9c84b8bfb events: ensure all models' __str__ can be called without any further lookups (#9480)
* events: ensure all models' __str__ can be called without any further lookups

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* allow for additional queries for models using default_token_key

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-27 22:19:33 +02:00
130adf9d26 core, web: update translations (#9482)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-04-27 22:19:20 +02:00
6aab505cd7 flows: fix execute API endpoint (#9478)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-27 01:56:59 +02:00
a9c597bc08 sources/oauth: fix OAuth Client sending token request incorrectly (#9474)
closes #9289

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 20:35:36 +02:00
853239dff9 web: bump API Client version (#9473)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-04-26 18:46:41 +02:00
8f8c3e4944 release: 2024.4.1 2024-04-26 18:43:33 +02:00
dde9960b9c website/docs: update release notes for 2024.4.1 again (#9471)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 17:38:42 +02:00
b1e48a6c1a sources/scim: fix service account user path (#9463)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 17:08:46 +02:00
b704e9031e web/admin: fix disabled button color with dark theme (#9465)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 16:53:57 +02:00
15ef5dc792 web/admin: show user internal service account as disabled (#9464)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 16:50:25 +02:00
6c4a1850b0 website/docs: prepare 2024.4.1 (#9459)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 14:38:35 +02:00
183d036f3c core: bump ruff from 0.4.1 to 0.4.2 (#9448)
* core: bump ruff from 0.4.1 to 0.4.2

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.1 to 0.4.2.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.1...v0.4.2)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix formatting

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 13:24:46 +02:00
b324dc0ce2 lifecycle: always try custom redis URL (#9441)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-26 13:24:36 +02:00
6ad7be65ec core, web: update translations (#9443)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-04-26 12:08:48 +02:00
8bf335a2a5 web: bump chromedriver from 123.0.4 to 124.0.1 in /tests/wdio (#9444)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 123.0.4 to 124.0.1.
- [Commits](https://github.com/giggio/node-chromedriver/compare/123.0.4...124.0.1)

---
updated-dependencies:
- dependency-name: chromedriver
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 12:08:30 +02:00
45709770f4 web: bump react-dom from 18.2.0 to 18.3.0 in /web (#9446)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 18.2.0 to 18.3.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  dependency-type: direct:development
  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>
2024-04-26 12:08:14 +02:00
6158dd80ca core: bump sentry-sdk from 1.45.0 to 2.0.0 (#9447)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 1.45.0 to 2.0.0.
- [Release notes](https://github.com/getsentry/sentry-python/releases)
- [Changelog](https://github.com/getsentry/sentry-python/blob/master/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-python/compare/1.45.0...2.0.0)

---
updated-dependencies:
- dependency-name: sentry-sdk
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-26 12:08:06 +02:00
468d26c587 core: bump black from 24.4.1 to 24.4.2 (#9449)
Bumps [black](https://github.com/psf/black) from 24.4.1 to 24.4.2.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.4.1...24.4.2)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  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>
2024-04-26 12:07:53 +02:00
c39a97ca58 website: bump react-dom from 18.2.0 to 18.3.0 in /website (#9450)
Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 18.2.0 to 18.3.0.
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react-dom)

---
updated-dependencies:
- dependency-name: react-dom
  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>
2024-04-26 12:07:39 +02:00
8f0810ebb3 website: bump react and @types/react in /website (#9451)
Bumps [react](https://github.com/facebook/react/tree/HEAD/packages/react) and [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react). These dependencies needed to be updated together.

Updates `react` from 18.2.0 to 18.3.0
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/facebook/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/HEAD/packages/react)

Updates `@types/react` from 18.2.79 to 18.3.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: react
  dependency-type: direct:production
  update-type: version-update:semver-minor
- dependency-name: "@types/react"
  dependency-type: direct:development
  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>
2024-04-26 12:07:17 +02:00
98e0f12d17 website/integrations: added documentation for globalprotect integration (#9368)
* website/integrations: added documentation for globalprotect integration

* Apply suggestions from code review

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: asc6 <chessmasterandy@cox.net>

---------

Signed-off-by: asc6 <chessmasterandy@cox.net>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-04-26 03:49:53 -05:00
8d37e83df7 web/common: fix locale detection for user-set locale (#9436)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 22:36:45 +02:00
a306bb8384 website/integrations: add FortiGate SSL VPN and Admin Login (#9105)
* PR for SSLVPN of Fortigate

Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* PR for Admin Login of Fortigate

Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* format and add to sidebar

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-ssl/index.md

thank you!

Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>

* Update website/integrations/services/fortigate-admin/index.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

* Update website/integrations/services/fortigate-ssl/index.md

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>

---------

Signed-off-by: NiceDevil <17103076+nicedevil007@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
2024-04-25 19:08:30 +00:00
ca42506fa0 Merge branch 'main' into dev
* main:
  web: clean up some repetitive types (#9241)
  core: fix logic for token expiration (#9426)
  ci: fix ci pipeline (#9427)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9424)
  web: Add resolved and integrity fields back to package-lock.json (#9419)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9407)
  stages/identification: don't check source component (#9410)
  core: bump selenium from 4.19.0 to 4.20.0 (#9411)
  core: bump black from 24.4.0 to 24.4.1 (#9412)
  ci: bump golangci/golangci-lint-action from 4 to 5 (#9413)
  core: bump goauthentik.io/api/v3 from 3.2024023.2 to 3.2024040.1 (#9414)
  web: bump @sentry/browser from 7.112.1 to 7.112.2 in /web in the sentry group (#9416)
  sources/oauth: ensure all UI sources return a valid source (#9401)
  web: markdown: display markdown even when frontmatter is missing (#9404)
2024-04-25 08:38:08 -07:00
c80116475b web: clean up some repetitive types (#9241)
* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: clean up some repetitive types

This commit centralizes two types that were defined multiple times throughout our code, and
casts in stone those definitions, applying the correct definitions where needed.

I had two types that were used repeatedly to define the interfaces for providers and context
consumers. Because they were both one-liners, I had done what I usually curse in others: copied
them. Worse, I hand-wrote them because they're so simple I had them memorized.
2024-04-25 08:28:05 -07:00
2997382df2 core: fix logic for token expiration (#9426)
* core: fix logic for token expiration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* bump default token expiration

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix frontend

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 15:42:58 +02:00
65e48907d3 ci: fix ci pipeline (#9427)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 15:42:39 +02:00
1c4848ed8f translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9424)
Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-04-25 15:31:26 +02:00
64f7fa62dd web: Add resolved and integrity fields back to package-lock.json (#9419)
* web: Fix missing resolved and integrity fields in package-lock.json

* web,website: Add lockfile lint to CI
2024-04-25 12:28:54 +02:00
16abaa8016 translate: Updates for file locale/en/LC_MESSAGES/django.po in ru (#9407)
Translate locale/en/LC_MESSAGES/django.po in ru

100% translated source file: 'locale/en/LC_MESSAGES/django.po'
on 'ru'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-04-25 11:59:18 +02:00
4cc4a3e4b8 stages/identification: don't check source component (#9410)
* Do not include the built-in source in this check

Signed-off-by: PythonCoderAS <13932583+PythonCoderAS@users.noreply.github.com>

* Update authentik/stages/identification/stage.py

Signed-off-by: Jens L. <jens@beryju.org>

---------

Signed-off-by: PythonCoderAS <13932583+PythonCoderAS@users.noreply.github.com>
Signed-off-by: Jens L. <jens@beryju.org>
Co-authored-by: Jens L <jens@beryju.org>
2024-04-25 11:55:31 +02:00
8abe1f61ea core: bump selenium from 4.19.0 to 4.20.0 (#9411)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.19.0 to 4.20.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.19.0...selenium-4.20.0)

---
updated-dependencies:
- dependency-name: selenium
  dependency-type: direct:development
  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>
2024-04-25 11:52:07 +02:00
6712095d7e core: bump black from 24.4.0 to 24.4.1 (#9412)
Bumps [black](https://github.com/psf/black) from 24.4.0 to 24.4.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.4.0...24.4.1)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:development
  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>
2024-04-25 11:52:00 +02:00
5ab308bfd7 ci: bump golangci/golangci-lint-action from 4 to 5 (#9413)
Bumps [golangci/golangci-lint-action](https://github.com/golangci/golangci-lint-action) from 4 to 5.
- [Release notes](https://github.com/golangci/golangci-lint-action/releases)
- [Commits](https://github.com/golangci/golangci-lint-action/compare/v4...v5)

---
updated-dependencies:
- dependency-name: golangci/golangci-lint-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 11:51:44 +02:00
8b93fbcc69 core: bump goauthentik.io/api/v3 from 3.2024023.2 to 3.2024040.1 (#9414)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024023.2 to 3.2024040.1.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024023.2...v3.2024040.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>
2024-04-25 11:51:35 +02:00
f641670139 web: bump @sentry/browser from 7.112.1 to 7.112.2 in /web in the sentry group (#9416)
web: bump @sentry/browser in /web in the sentry group

Bumps the sentry group in /web with 1 update: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 7.112.1 to 7.112.2
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.112.2/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.112.1...7.112.2)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-25 11:51:27 +02:00
80af26ef50 sources/oauth: ensure all UI sources return a valid source (#9401)
* web/admin: prevent selection of inbuilt source in identification stage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix apple source

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* also fix plex challenge

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-24 22:55:19 +02:00
64ce170882 web: markdown: display markdown even when frontmatter is missing (#9404)
* web: fix esbuild issue with style sheets

Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.

* web: markdown: display markdown even when frontmatter is missing

Make the check for the document title comprehensive across the
entire demeter.  If there is no front matter, `data` will be missing,
not just `data.title`.
2024-04-24 22:53:18 +02:00
34de6bfd3a Merge branch 'main' into dev
* main:
  web: bump API Client version (#9400)
  release: 2024.4.0
  release: 2024.4.0-rc1
  root: bump blueprint schema version
  lifecycle: fix ak test-all command
  website/docs: finalize 2024.4 release notes (#9396)
  web: bump @sentry/browser from 7.111.0 to 7.112.1 in /web in the sentry group (#9387)
  web: bump the rollup group in /web with 3 updates (#9388)
  ci: bump helm/kind-action from 1.9.0 to 1.10.0 (#9389)
  website: bump clsx from 2.1.0 to 2.1.1 in /website (#9390)
  core: bump pydantic from 2.7.0 to 2.7.1 (#9391)
  core: bump freezegun from 1.4.0 to 1.5.0 (#9393)
  core: bump coverage from 7.4.4 to 7.5.0 (#9392)
  web: bump the storybook group in /web with 7 updates (#9380)
  web: bump the rollup group in /web with 3 updates (#9381)
2024-04-24 13:20:02 -07:00
b6171aa1a4 web: bump API Client version (#9400)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: authentik-automation[bot] <135050075+authentik-automation[bot]@users.noreply.github.com>
2024-04-24 19:34:56 +02:00
087582abbd release: 2024.4.0 2024-04-24 19:12:50 +02:00
6b6d88b81b release: 2024.4.0-rc1 2024-04-24 19:12:47 +02:00
55e5d36df5 root: bump blueprint schema version
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-24 19:11:54 +02:00
fc43e841c9 lifecycle: fix ak test-all command
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-24 19:11:53 +02:00
895ed6fbdc website/docs: finalize 2024.4 release notes (#9396)
* website/docs: finalize 2024.4 release notes

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* escape curly braces manually

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-24 17:40:35 +02:00
f3965261c5 web: bump @sentry/browser from 7.111.0 to 7.112.1 in /web in the sentry group (#9387)
web: bump @sentry/browser in /web in the sentry group

Bumps the sentry group in /web with 1 update: [@sentry/browser](https://github.com/getsentry/sentry-javascript).


Updates `@sentry/browser` from 7.111.0 to 7.112.1
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.112.1/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.111.0...7.112.1)

---
updated-dependencies:
- dependency-name: "@sentry/browser"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: sentry
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 13:44:32 +02:00
34ee6dc2b7 web: bump the rollup group in /web with 3 updates (#9388)
Bumps the rollup group in /web with 3 updates: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) and [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.16.2 to 4.16.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.2...v4.16.4)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.16.2 to 4.16.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.2...v4.16.4)

Updates `@rollup/rollup-linux-x64-gnu` from 4.16.2 to 4.16.4
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.2...v4.16.4)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-24 11:08:58 +02:00
55fe4b0bc0 ci: bump helm/kind-action from 1.9.0 to 1.10.0 (#9389)
Bumps [helm/kind-action](https://github.com/helm/kind-action) from 1.9.0 to 1.10.0.
- [Release notes](https://github.com/helm/kind-action/releases)
- [Commits](https://github.com/helm/kind-action/compare/v1.9.0...v1.10.0)

---
updated-dependencies:
- dependency-name: helm/kind-action
  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>
2024-04-24 11:08:51 +02:00
8d745609f9 website: bump clsx from 2.1.0 to 2.1.1 in /website (#9390)
Bumps [clsx](https://github.com/lukeed/clsx) from 2.1.0 to 2.1.1.
- [Release notes](https://github.com/lukeed/clsx/releases)
- [Commits](https://github.com/lukeed/clsx/compare/v2.1.0...v2.1.1)

---
updated-dependencies:
- dependency-name: clsx
  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>
2024-04-24 11:08:41 +02:00
55edb10da0 core: bump pydantic from 2.7.0 to 2.7.1 (#9391)
Bumps [pydantic](https://github.com/pydantic/pydantic) from 2.7.0 to 2.7.1.
- [Release notes](https://github.com/pydantic/pydantic/releases)
- [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md)
- [Commits](https://github.com/pydantic/pydantic/compare/v2.7.0...v2.7.1)

---
updated-dependencies:
- dependency-name: pydantic
  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>
2024-04-24 11:08:32 +02:00
66e4b3af36 core: bump freezegun from 1.4.0 to 1.5.0 (#9393)
Bumps [freezegun](https://github.com/spulec/freezegun) from 1.4.0 to 1.5.0.
- [Release notes](https://github.com/spulec/freezegun/releases)
- [Changelog](https://github.com/spulec/freezegun/blob/master/CHANGELOG)
- [Commits](https://github.com/spulec/freezegun/compare/1.4.0...1.5.0)

---
updated-dependencies:
- dependency-name: freezegun
  dependency-type: direct:development
  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>
2024-04-24 11:08:11 +02:00
d44fc7790e core: bump coverage from 7.4.4 to 7.5.0 (#9392)
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.4.4 to 7.5.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.4.4...7.5.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:development
  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>
2024-04-24 11:08:01 +02:00
291972628a web: bump the storybook group in /web with 7 updates (#9380)
Bumps the storybook group in /web with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [@storybook/addon-essentials](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/essentials) | `8.0.8` | `8.0.9` |
| [@storybook/addon-links](https://github.com/storybookjs/storybook/tree/HEAD/code/addons/links) | `8.0.8` | `8.0.9` |
| [@storybook/blocks](https://github.com/storybookjs/storybook/tree/HEAD/code/ui/blocks) | `8.0.8` | `8.0.9` |
| [@storybook/manager-api](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/manager-api) | `8.0.8` | `8.0.9` |
| [@storybook/web-components](https://github.com/storybookjs/storybook/tree/HEAD/code/renderers/web-components) | `8.0.8` | `8.0.9` |
| [@storybook/web-components-vite](https://github.com/storybookjs/storybook/tree/HEAD/code/frameworks/web-components-vite) | `8.0.8` | `8.0.9` |
| [storybook](https://github.com/storybookjs/storybook/tree/HEAD/code/lib/cli) | `8.0.8` | `8.0.9` |


Updates `@storybook/addon-essentials` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/addons/essentials)

Updates `@storybook/addon-links` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/addons/links)

Updates `@storybook/blocks` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/ui/blocks)

Updates `@storybook/manager-api` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/lib/manager-api)

Updates `@storybook/web-components` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/frameworks/web-components-vite)

Updates `storybook` from 8.0.8 to 8.0.9
- [Release notes](https://github.com/storybookjs/storybook/releases)
- [Changelog](https://github.com/storybookjs/storybook/blob/next/CHANGELOG.md)
- [Commits](https://github.com/storybookjs/storybook/commits/v8.0.9/code/lib/cli)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/blocks"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/manager-api"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
- dependency-name: storybook
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: storybook
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 11:51:52 +02:00
019221c433 web: bump the rollup group in /web with 3 updates (#9381)
Bumps the rollup group in /web with 3 updates: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) and [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.16.1 to 4.16.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.1...v4.16.2)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.16.1 to 4.16.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.1...v4.16.2)

Updates `@rollup/rollup-linux-x64-gnu` from 4.16.1 to 4.16.2
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.16.1...v4.16.2)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-23 11:30:37 +02:00
2d94b16411 Merge branch 'main' into dev
* main: (24 commits)
  web: bump the wdio group in /tests/wdio with 4 updates (#9374)
  web: bump the rollup group in /web with 3 updates (#9371)
  core: bump ruff from 0.4.0 to 0.4.1 (#9372)
  core, web: update translations (#9366)
  web/admin: fix document title for admin interface (#9362)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#9363)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#9364)
  core, web: update translations (#9360)
  website/docs: release notes 2024.4: add performance improvements values (#9356)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#9317)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#9318)
  website/docs: 2024.4 release notes (#9267)
  sources/ldap: fix default blueprint for mapping user DN to path (#9355)
  web/admin: group form dual select (#9354)
  core: bump golang.org/x/net from 0.22.0 to 0.23.0 (#9351)
  core: bump goauthentik.io/api/v3 from 3.2024023.1 to 3.2024023.2 (#9345)
  web: bump chromedriver from 123.0.3 to 123.0.4 in /tests/wdio (#9348)
  core: bump twilio from 9.0.4 to 9.0.5 (#9346)
  core: bump ruff from 0.3.7 to 0.4.0 (#9347)
  web: bump @sentry/browser from 7.110.1 to 7.111.0 in /web in the sentry group (#9349)
  ...
2024-04-22 08:53:56 -07:00
b99fa9f8f8 web: bump the wdio group in /tests/wdio with 4 updates (#9374)
Bumps the wdio group in /tests/wdio with 4 updates: [@wdio/cli](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-cli), [@wdio/local-runner](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-local-runner), [@wdio/mocha-framework](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-mocha-framework) and [@wdio/spec-reporter](https://github.com/webdriverio/webdriverio/tree/HEAD/packages/wdio-spec-reporter).


Updates `@wdio/cli` from 8.36.0 to 8.36.1
- [Release notes](https://github.com/webdriverio/webdriverio/releases)
- [Changelog](https://github.com/webdriverio/webdriverio/blob/v8.36.1/CHANGELOG.md)
- [Commits](https://github.com/webdriverio/webdriverio/commits/v8.36.1/packages/wdio-cli)

Updates `@wdio/local-runner` from 8.36.0 to 8.36.1
- [Release notes](https://github.com/webdriverio/webdriverio/releases)
- [Changelog](https://github.com/webdriverio/webdriverio/blob/v8.36.1/CHANGELOG.md)
- [Commits](https://github.com/webdriverio/webdriverio/commits/v8.36.1/packages/wdio-local-runner)

Updates `@wdio/mocha-framework` from 8.36.0 to 8.36.1
- [Release notes](https://github.com/webdriverio/webdriverio/releases)
- [Changelog](https://github.com/webdriverio/webdriverio/blob/v8.36.1/CHANGELOG.md)
- [Commits](https://github.com/webdriverio/webdriverio/commits/v8.36.1/packages/wdio-mocha-framework)

Updates `@wdio/spec-reporter` from 8.36.0 to 8.36.1
- [Release notes](https://github.com/webdriverio/webdriverio/releases)
- [Changelog](https://github.com/webdriverio/webdriverio/blob/v8.36.1/CHANGELOG.md)
- [Commits](https://github.com/webdriverio/webdriverio/commits/v8.36.1/packages/wdio-spec-reporter)

---
updated-dependencies:
- dependency-name: "@wdio/cli"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: wdio
- dependency-name: "@wdio/local-runner"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: wdio
- dependency-name: "@wdio/mocha-framework"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: wdio
- dependency-name: "@wdio/spec-reporter"
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: wdio
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 12:06:54 +02:00
5bde2772c3 web: bump the rollup group in /web with 3 updates (#9371)
Bumps the rollup group in /web with 3 updates: [@rollup/rollup-darwin-arm64](https://github.com/rollup/rollup), [@rollup/rollup-linux-arm64-gnu](https://github.com/rollup/rollup) and [@rollup/rollup-linux-x64-gnu](https://github.com/rollup/rollup).


Updates `@rollup/rollup-darwin-arm64` from 4.14.3 to 4.16.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.14.3...v4.16.1)

Updates `@rollup/rollup-linux-arm64-gnu` from 4.14.3 to 4.16.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.14.3...v4.16.1)

Updates `@rollup/rollup-linux-x64-gnu` from 4.14.3 to 4.16.1
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.14.3...v4.16.1)

---
updated-dependencies:
- dependency-name: "@rollup/rollup-darwin-arm64"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-arm64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rollup
- dependency-name: "@rollup/rollup-linux-x64-gnu"
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rollup
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-22 12:06:11 +02:00
10884a7770 core: bump ruff from 0.4.0 to 0.4.1 (#9372)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.0 to 0.4.1.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md)
- [Commits](https://github.com/astral-sh/ruff/compare/v0.4.0...v0.4.1)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:development
  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>
2024-04-22 12:05:52 +02:00
e858d09d28 core, web: update translations (#9366)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-04-21 14:29:30 +02:00
856717395e web/admin: fix document title for admin interface (#9362)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-20 22:55:41 +02:00
b7793200de translate: Updates for file web/xliff/en.xlf in zh_CN (#9363)
Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-04-20 22:55:30 +02:00
bcc0323523 translate: Updates for file web/xliff/en.xlf in zh-Hans (#9364)
Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-04-20 22:55:16 +02:00
643c1f5bbf core, web: update translations (#9360)
Signed-off-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: rissson <18313093+rissson@users.noreply.github.com>
2024-04-20 15:31:49 +02:00
1fca246839 website/docs: release notes 2024.4: add performance improvements values (#9356) 2024-04-19 16:36:47 +00:00
b73e68a94c translate: Updates for file web/xliff/en.xlf in zh_CN (#9317)
Translate web/xliff/en.xlf in zh_CN

100% translated source file: 'web/xliff/en.xlf'
on 'zh_CN'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-04-19 17:18:30 +02:00
f9d3c4c9a7 translate: Updates for file web/xliff/en.xlf in zh-Hans (#9318)
Translate web/xliff/en.xlf in zh-Hans

100% translated source file: 'web/xliff/en.xlf'
on 'zh-Hans'.

Co-authored-by: transifex-integration[bot] <43880903+transifex-integration[bot]@users.noreply.github.com>
2024-04-19 17:18:08 +02:00
98503f6009 Merge branch 'main' into dev
* main:
  stages/prompt: fix username field throwing error with existing user (#9342)
  root: expose session storage configuration (#9337)
  website/integrations: fix typo (#9340)
  root: fix go.mod for codeql checking (#9338)
  root: make redis settings more consistent (#9335)
  web/admin: fix error in admin interface due to un-hydrated context (#9336)
  web: bump API Client version (#9334)
  stages/authenticator_webauthn: fix attestation value (#9333)
  website/docs: fix SECRET_KEY length (#9328)
  website/docs: fix email template formatting (#9330)
  core, web: update translations (#9323)
  web: bump @patternfly/elements from 3.0.0 to 3.0.1 in /web (#9324)
  core: bump celery from 5.3.6 to 5.4.0 (#9325)
  core: bump goauthentik.io/api/v3 from 3.2024022.12 to 3.2024023.1 (#9327)
  sources/scim: service account should be internal (#9321)
  web: bump the storybook group in /web with 8 updates (#9266)
  sources/scim: cleanup service account when source is deleted (#9319)
2024-04-18 11:55:29 -07:00
ac4ba5d9e2 Merge branch 'main' into dev
* main: (23 commits)
  web: bump API Client version (#9316)
  release: 2024.2.3
  website/docs: 2024.2.3 release notes (#9313)
  web/admin: fix log viewer empty state (#9315)
  website/docs: fix formatting for stage changes (#9314)
  core: bump github.com/go-ldap/ldap/v3 from 3.4.7 to 3.4.8 (#9310)
  core: bump goauthentik.io/api/v3 from 3.2024022.11 to 3.2024022.12 (#9311)
  web: bump core-js from 3.36.1 to 3.37.0 in /web (#9309)
  core: bump gunicorn from 21.2.0 to 22.0.0 (#9308)
  core, web: update translations (#9307)
  website/docs: system settings: add default token duration and length (#9306)
  web/flows: update flow background (#9305)
  web: fix locale loading being skipped (#9301)
  translate: Updates for file web/xliff/en.xlf in fr (#9304)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#9303)
  core: replace authentik_signals_ignored_fields with audit_ignore (#9291)
  web/flow: fix form input rendering issue (#9297)
  events: fix incorrect user logged when using API token authentication (#9302)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9293)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9295)
  ...
2024-04-17 10:50:26 -07:00
f19ed14bf8 Merge branch 'main' into dev
* main: (34 commits)
  web: bump API Client version (#9299)
  core: fix api schema for users and groups (#9298)
  providers/oauth2: fix refresh_token grant returning incorrect id_token (#9275)
  web: bump @sentry/browser from 7.110.0 to 7.110.1 in /web in the sentry group (#9278)
  core, web: update translations (#9277)
  web: bump the rollup group in /web with 3 updates (#9280)
  web: bump lit from 3.1.2 to 3.1.3 in /web (#9282)
  web: bump @lit/context from 1.1.0 to 1.1.1 in /web (#9281)
  website: bump @types/react from 18.2.78 to 18.2.79 in /website (#9286)
  core: bump goauthentik.io/api/v3 from 3.2024022.10 to 3.2024022.11 (#9285)
  core: bump sqlparse from 0.4.4 to 0.5.0 (#9276)
  lifecycle: gunicorn: fix app preload (#9274)
  events: add indexes (#9272)
  web/flows: fix passwordless hidden without input (#9273)
  root: fix geoipupdate arguments (#9271)
  website/docs: cleanup more (#9249)
  web: bump API Client version (#9270)
  sources: add SCIM source (#3051)
  core: delegated group member management (#9254)
  web: bump API Client version (#9269)
  ...
2024-04-16 10:49:58 -07:00
085debf170 Merge branch 'main' into dev
* main: (21 commits)
  web: manage stacked modals with a stack (#9193)
  website/docs: ensure yaml code blocks have language tags (#9240)
  blueprints: only create default brand if no other default brand exists (#9222)
  web: bump API Client version (#9239)
  website/integrations: portainer: Fix Redirect URL mismatch (#9226)
  api: fix authentication schema (#9238)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#9229)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#9230)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9228)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9231)
  core: bump pydantic from 2.6.4 to 2.7.0 (#9232)
  core: bump ruff from 0.3.5 to 0.3.7 (#9233)
  web: bump @sentry/browser from 7.109.0 to 7.110.0 in /web in the sentry group (#9234)
  website: bump @types/react from 18.2.75 to 18.2.77 in /website (#9236)
  core, web: update translations (#9225)
  website/integrations: add pfSense search scope (#9221)
  core: bump idna from 3.6 to 3.7 (#9224)
  website/docs: add websocket support to nginx snippets (#9220)
  internal: add tests to go flow executor (#9219)
  website/integrations: nextcloud: add tip to solve hashed groups configuring OAuth2 (#9153)
  ...
2024-04-12 14:27:20 -07:00
cacdf64408 Merge branch 'main' into dev
* main:
  website/docs: add more info and links about enforciing unique email addresses (#9154)
  core: bump goauthentik.io/api/v3 from 3.2024022.7 to 3.2024022.8 (#9215)
  web: bump API Client version (#9214)
  stages/authenticator_validate: add ability to limit webauthn device types (#9180)
  web: bump API Client version (#9213)
  core: add user settable token durations (#7410)
  core, web: update translations (#9205)
  web: bump typescript from 5.4.4 to 5.4.5 in /tests/wdio (#9206)
  web: bump chromedriver from 123.0.2 to 123.0.3 in /tests/wdio (#9207)
  core: bump sentry-sdk from 1.44.1 to 1.45.0 (#9208)
  web: bump typescript from 5.4.4 to 5.4.5 in /web (#9209)
  website: bump typescript from 5.4.4 to 5.4.5 in /website (#9210)
  core: bump python from 3.12.2-slim-bookworm to 3.12.3-slim-bookworm (#9211)
2024-04-11 08:10:41 -07:00
23665d173f Merge branch 'main' into dev
* main:
  website/docs: add note for flow compatibility mode (#9204)
2024-04-10 13:53:58 -07:00
272fdc516b Merge branch 'main' into dev
* main:
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9194)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9197)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#9196)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#9198)
  web: preserve selected list when provider updates (#9200)
  web: bump API Client version (#9195)
  sources/oauth: make URLs not required, only check when no OIDC URLs are defined (#9182)
2024-04-10 08:17:38 -07:00
b08dcc2289 Merge branch 'main' into dev
* main:
  web/admin: fix SAML Provider preview (#9192)
  core, web: update translations (#9183)
  web: bump chromedriver from 123.0.1 to 123.0.2 in /tests/wdio (#9188)
  website: bump @types/react from 18.2.74 to 18.2.75 in /website (#9185)
  website/docs: update Postgresql username (#9190)
  core: bump maxmind/geoipupdate from v6.1 to v7.0 (#9186)
  events: add context manager to ignore/modify audit events being written (#9181)
  web: fix application library list display length and capability (#9094)
2024-04-09 08:47:08 -07:00
c84be1d961 Merge branch 'main' into dev
* main: (25 commits)
  root: fix readme (#9178)
  enterprise: fix audit middleware import (#9177)
  web: bump @spotlightjs/spotlight from 1.2.16 to 1.2.17 in /web in the sentry group (#9162)
  web: bump API Client version (#9174)
  stages/authenticator_webauthn: add MDS support (#9114)
  website/integrations: Update Nextcloud OIDC secret size limitation (#9139)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#9170)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#9171)
  web: bump the rollup group in /web with 3 updates (#9164)
  web: bump @codemirror/legacy-modes from 6.3.3 to 6.4.0 in /web (#9166)
  web: bump ts-pattern from 5.1.0 to 5.1.1 in /web (#9167)
  core: bump github.com/go-ldap/ldap/v3 from 3.4.6 to 3.4.7 (#9168)
  core, web: update translations (#9156)
  root: fix redis username in lifecycle (#9158)
  web: ak-checkbox-group for short, static, multi-select events (#9138)
  root: fix startup (#9151)
  core: Bump golang.org/x/oauth2 from 0.18.0 to 0.19.0 (#9146)
  core: Bump twilio from 9.0.3 to 9.0.4 (#9143)
  web: Bump country-flag-icons from 1.5.10 to 1.5.11 in /web (#9144)
  web: Bump typescript from 5.4.3 to 5.4.4 in /web (#9145)
  ...
2024-04-08 09:22:54 -07:00
875fc5c735 Merge branch 'main' into dev
* main: (22 commits)
  blueprints: fix default username field in user-settings flow (#9136)
  website/docs: add procedural docs for RAC (#9006)
  web: bump API Client version (#9133)
  ci: fix python client generator (#9134)
  root: generate python client (#9107)
  web: Bump vite from 5.1.4 to 5.2.8 in /web (#9120)
  core, web: update translations (#9124)
  core: Bump golang from 1.22.1-bookworm to 1.22.2-bookworm (#9125)
  web: Bump the babel group in /web with 2 updates (#9126)
  web: Bump the eslint group in /web with 1 update (#9127)
  web: Bump the eslint group in /tests/wdio with 1 update (#9129)
  core: Bump sentry-sdk from 1.44.0 to 1.44.1 (#9130)
  core: Bump channels from 4.0.0 to 4.1.0 (#9131)
  core: Bump django from 5.0.3 to 5.0.4 (#9132)
  web: Bump the rollup group in /web with 3 updates (#9128)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9110)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9109)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#9111)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#9112)
  web: Bump @fortawesome/fontawesome-free from 6.5.1 to 6.5.2 in /web (#9116)
  ...
2024-04-04 10:54:11 -07:00
66cefcc918 Merge branch 'main' into dev
* main:
  root: fix missing imports after #9081 (#9106)
  root: move database calls from ready() to dedicated startup signal (#9081)
  web: fix console log leftover (#9096)
  web: bump the eslint group in /web with 2 updates (#9098)
  core: bump twilio from 9.0.2 to 9.0.3 (#9103)
  web: bump the eslint group in /tests/wdio with 2 updates (#9099)
  core: bump drf-spectacular from 0.27.1 to 0.27.2 (#9100)
  core: bump django-model-utils from 4.4.0 to 4.5.0 (#9101)
  core: bump ruff from 0.3.4 to 0.3.5 (#9102)
  website/docs:  update notes on SECRET_KEY (#9091)
  web: fix broken locale compile (#9095)
  website/integrations: add outline knowledge base (#8786)
  website/docs: fix typo (#9082)
  website/docs: email stage: fix example translation error (#9048)
2024-04-02 09:01:01 -07:00
5d4c38032f Merge branch 'main' into dev
* main:
  web: bump @patternfly/elements from 2.4.0 to 3.0.0 in /web (#9089)
  web: bump ts-pattern from 5.0.8 to 5.1.0 in /web (#9090)
  website: bump the docusaurus group in /website with 9 updates (#9087)
  web/admin: allow custom sorting for bound* tables (#9080)
2024-04-01 08:31:33 -07:00
7123b2c57b Merge branch 'main' into dev
* main:
  web: move context controllers into reactive controller plugins (#8996)
  web: maintenance: split tsconfig into “base” and “build” variants. (#9036)
  web: consistent style declarations internally (#9077)
2024-03-29 13:02:47 -07:00
fc00bdee63 Merge branch 'main' into dev
* main: (23 commits)
  providers/oauth2: fix interactive device flow (#9076)
  website/docs: fix transports example (#9074)
  events: fix log_capture (#9075)
  web: bump the sentry group in /web with 2 updates (#9065)
  core: bump goauthentik.io/api/v3 from 3.2024022.6 to 3.2024022.7 (#9064)
  web: bump @codemirror/lang-python from 6.1.4 to 6.1.5 in /web (#9068)
  web: bump the eslint group in /web with 1 update (#9066)
  web: bump glob from 10.3.10 to 10.3.12 in /web (#9069)
  web: bump the rollup group in /web with 3 updates (#9067)
  web: bump the eslint group in /tests/wdio with 1 update (#9071)
  core: bump webauthn from 2.0.0 to 2.1.0 (#9070)
  core: bump sentry-sdk from 1.43.0 to 1.44.0 (#9073)
  core: bump requests-mock from 1.12.0 to 1.12.1 (#9072)
  web: bump API Client version (#9061)
  events: rework log messages returned from API and their rendering (#8770)
  website/docs: update airgapped config (#9049)
  website: bump @types/react from 18.2.72 to 18.2.73 in /website (#9052)
  web: bump the rollup group in /web with 3 updates (#9053)
  core: bump django-filter from 24.1 to 24.2 (#9055)
  core: bump requests-mock from 1.11.0 to 1.12.0 (#9056)
  ...
2024-03-29 08:35:41 -07:00
a056703da0 Merge branch 'main' into dev
* main:
  web: a few minor bugfixes and lintfixes (#9044)
  website/integrations: add documentation for OIDC setup with Xen Orchestra (#9000)
  website: bump @types/react from 18.2.70 to 18.2.72 in /website (#9041)
  core: bump goauthentik.io/api/v3 from 3.2024022.5 to 3.2024022.6 (#9042)
  web: fix markdown rendering bug for alerts (#9037)
2024-03-27 10:51:02 -07:00
3f9502072d Merge branch 'main' into dev
* main:
  web: bump API Client version (#9035)
  website/docs: maintenance, re-add system settings (#9026)
  core: bump duo-client from 5.2.0 to 5.3.0 (#9029)
  website: bump express from 4.18.2 to 4.19.2 in /website (#9027)
  web: bump express from 4.18.3 to 4.19.2 in /web (#9028)
  web: bump the eslint group in /web with 2 updates (#9030)
  core: bump goauthentik.io/api/v3 from 3.2024022.3 to 3.2024022.5 (#9031)
  website: bump @types/react from 18.2.69 to 18.2.70 in /website (#9032)
  web: bump the eslint group in /tests/wdio with 2 updates (#9033)
  web: bump katex from 0.16.9 to 0.16.10 in /web (#9025)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#9023)
  website/docs: include OS-specific docker-compose install instructions + minor fixes (#8975)
2024-03-26 08:58:18 -07:00
2d254d6a7e Merge branch 'main' into dev
* main:
  web: bump API Client version (#9021)
  sources/ldap: add ability to disable password write on login (#8377)
  web: bump API Client version (#9020)
  lifecycle: migrate: ensure template schema exists before migrating (#8952)
  website/integrations: Update nextcloud Admin Group Expression (#7314)
  web/flow: general ux improvements (#8558)
  website: bump @types/react from 18.2.67 to 18.2.69 in /website (#9016)
  core: bump requests-oauthlib from 1.4.0 to 2.0.0 (#9018)
  web: bump the sentry group in /web with 2 updates (#9017)
  web/admin: small fixes (#9002)
  website: bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /website (#9001)
  core: bump ruff from 0.3.3 to 0.3.4 (#8998)
  website/docs: Upgrade nginx reverse porxy config (#8947)
  website/docs: improve flow inspector docs (#8993)
  website/deverlop-docs website/integrations: add links to integrations template (#8995)
2024-03-25 07:44:17 -07:00
a7e3dca917 Merge branch 'main' into dev
* main:
  website/docs: add example policy to enforce unique email address (#8955)
  web/admin: remove enterprise preview banner (#8991)
  core: bump uvicorn from 0.28.1 to 0.29.0 (#8980)
  core: bump sentry-sdk from 1.42.0 to 1.43.0 (#8981)
  web: bump the babel group in /web with 3 updates (#8983)
  web: bump typescript from 5.4.2 to 5.4.3 in /web (#8984)
  web: bump typescript from 5.4.2 to 5.4.3 in /tests/wdio (#8986)
  web: bump chromedriver from 122.0.6 to 123.0.0 in /tests/wdio (#8987)
  website: bump typescript from 5.4.2 to 5.4.3 in /website (#8989)
  core: bump importlib-metadata from 7.0.2 to 7.1.0 (#8982)
  web: bump the wdio group in /tests/wdio with 3 updates (#8985)
  website: bump postcss from 8.4.37 to 8.4.38 in /website (#8988)
2024-03-21 09:10:21 -07:00
5d8408287f Merge branch 'main' into dev
* main:
  website/docs: config: remove options moved to tenants (#8976)
  web: bump @types/grecaptcha from 3.0.8 to 3.0.9 in /web (#8971)
  web: bump country-flag-icons from 1.5.9 to 1.5.10 in /web (#8970)
  web: bump the babel group in /web with 7 updates (#8969)
  core: bump uvicorn from 0.28.0 to 0.28.1 (#8968)
  website: bump postcss from 8.4.36 to 8.4.37 in /website (#8967)
  internal: cleanup static file serving setup code (#8965)
  website/integrations: portainer: match portainer settings order (#8974)
2024-03-20 10:12:34 -07:00
30beca9118 Merge branch 'main' into dev
* main:
  web: improve build speeds even moar!!!!!! (#8954)
2024-03-19 14:37:17 -07:00
8946b81dbd Merge branch 'main' into dev
* main:
  outposts/proxy: Fix invalid redirect on external hosts containing path components (#8915)
  core: cache user application list under policies (#8895)
  web: bump the eslint group in /web with 2 updates (#8959)
  web: bump core-js from 3.36.0 to 3.36.1 in /web (#8960)
  website: bump @types/react from 18.2.66 to 18.2.67 in /website (#8962)
  web: bump the eslint group in /tests/wdio with 2 updates (#8963)
2024-03-19 14:36:12 -07:00
db96e1a901 Merge branch 'main' into dev
* main: (31 commits)
  root: support redis username (#8935)
  core: bump black from 24.2.0 to 24.3.0 (#8945)
  web: bump the wdio group in /tests/wdio with 2 updates (#8939)
  web: bump the sentry group in /web with 1 update (#8941)
  website: bump postcss from 8.4.35 to 8.4.36 in /website (#8940)
  core: bump twilio from 9.0.1 to 9.0.2 (#8942)
  core: bump ruff from 0.3.2 to 0.3.3 (#8943)
  events: discard notification if user has empty email (#8938)
  ci: always run ci-main on branch pushes (#8950)
  core: bump goauthentik.io/api/v3 from 3.2024022.2 to 3.2024022.3 (#8946)
  website/docs: add new name "Microsft Entra ID" for Azure AD  (#8930)
  outposts: Enhance config options for k8s outposts (#7363)
  website/docs: add link to CRUD docs (#8925)
  web: bump API Client version (#8927)
  outpost: improved set secret answers for flow execution (#8013)
  stages/user_write: ensure user data is json-serializable (#8926)
  website/docs: update example ldapsearch commands (#8906)
  admin: Handle latest  version unknown in admin dashboard (#8858)
  core: bump coverage from 7.4.3 to 7.4.4 (#8917)
  core: bump urllib3 from 1.26.18 to 2.2.1 (#8918)
  ...
2024-03-18 07:58:44 -07:00
8b4e0361c4 Merge branch 'main' into dev
* main:
  web: clean up and remove redundant alias '@goauthentik/app' (#8889)
  web/admin: fix markdown table rendering (#8908)
2024-03-14 10:35:46 -07:00
22cb5b7379 Merge branch 'main' into dev
* main:
  web: bump chromedriver from 122.0.5 to 122.0.6 in /tests/wdio (#8902)
  web: bump vite-tsconfig-paths from 4.3.1 to 4.3.2 in /web (#8903)
  core: bump google.golang.org/protobuf from 1.32.0 to 1.33.0 (#8901)
  web: provide InstallID on EnterpriseListPage (#8898)
2024-03-14 08:14:43 -07:00
2d0117d096 Merge branch 'main' into dev
* main:
  api: capabilities: properly set can_save_media when s3 is enabled (#8896)
  web: bump the rollup group in /web with 3 updates (#8891)
  core: bump pydantic from 2.6.3 to 2.6.4 (#8892)
  core: bump twilio from 9.0.0 to 9.0.1 (#8893)
2024-03-13 14:05:11 -07:00
035bda4eac Merge branch 'main' into dev
* main:
  Update _envoy_istio.md (#8888)
  website/docs: new landing page for Providers (#8879)
  web: bump the sentry group in /web with 1 update (#8881)
  web: bump chromedriver from 122.0.4 to 122.0.5 in /tests/wdio (#8884)
  web: bump the eslint group in /tests/wdio with 2 updates (#8883)
  web: bump the eslint group in /web with 2 updates (#8885)
  website: bump @types/react from 18.2.64 to 18.2.65 in /website (#8886)
2024-03-12 13:31:35 -07:00
50906214e5 Merge branch 'main' into dev
* main:
  web: upgrade to lit 3 (#8781)
2024-03-11 11:03:04 -07:00
e505f274b6 Merge branch 'main' into dev
* main:
  web: fix esbuild issue with style sheets (#8856)
2024-03-11 10:28:05 -07:00
fe52f44dca Merge branch 'main' into dev
* main:
  tenants: really ensure default tenant cannot be deleted (#8875)
  core: bump github.com/go-openapi/runtime from 0.27.2 to 0.28.0 (#8867)
  core: bump pytest from 8.0.2 to 8.1.1 (#8868)
  core: bump github.com/go-openapi/strfmt from 0.22.2 to 0.23.0 (#8869)
  core: bump bandit from 1.7.7 to 1.7.8 (#8870)
  core: bump packaging from 23.2 to 24.0 (#8871)
  core: bump ruff from 0.3.1 to 0.3.2 (#8873)
  web: bump the wdio group in /tests/wdio with 3 updates (#8865)
  core: bump requests-oauthlib from 1.3.1 to 1.4.0 (#8866)
  core: bump uvicorn from 0.27.1 to 0.28.0 (#8872)
  core: bump django-filter from 23.5 to 24.1 (#8874)
2024-03-11 10:27:43 -07:00
3146e5a50f web: fix esbuild issue with style sheets
Getting ESBuild, Lit, and Storybook to all agree on how to read and parse stylesheets is a serious
pain. This fix better identifies the value types (instances) being passed from various sources in
the repo to the three *different* kinds of style processors we're using (the native one, the
polyfill one, and whatever the heck Storybook does internally).

Falling back to using older CSS instantiating techniques one era at a time seems to do the trick.
It's ugly, but in the face of the aggressive styling we use to avoid Flashes of Unstyled Content
(FLoUC), it's the logic with which we're left.

In standard mode, the following warning appears on the console when running a Flow:

```
Autofocus processing was blocked because a document already has a focused element.
```

In compatibility mode, the following **error** appears on the console when running a Flow:

```
crawler-inject.js:1106 Uncaught TypeError: Failed to execute 'observe' on 'MutationObserver': parameter 1 is not of type 'Node'.
    at initDomMutationObservers (crawler-inject.js:1106:18)
    at crawler-inject.js:1114:24
    at Array.forEach (<anonymous>)
    at initDomMutationObservers (crawler-inject.js:1114:10)
    at crawler-inject.js:1549:1
initDomMutationObservers @ crawler-inject.js:1106
(anonymous) @ crawler-inject.js:1114
initDomMutationObservers @ crawler-inject.js:1114
(anonymous) @ crawler-inject.js:1549
```

Despite this error, nothing seems to be broken and flows work as anticipated.
2024-03-08 14:15:55 -08:00
328 changed files with 31891 additions and 6744 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.2.3
current_version = 2024.4.2
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*))?
@ -21,6 +21,8 @@ optional_value = final
[bumpversion:file:schema.yml]
[bumpversion:file:blueprints/schema.json]
[bumpversion:file:authentik/__init__.py]
[bumpversion:file:internal/constants/constants.go]

View File

@ -12,7 +12,7 @@ should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower()
branch_name = os.environ["GITHUB_REF"]
if os.environ.get("GITHUB_HEAD_REF", "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"]
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-")
image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None
@ -54,9 +54,9 @@ image_main_tag = image_tags[0]
image_tags_rendered = ",".join(image_tags)
with open(os.environ["GITHUB_OUTPUT"], "a+", encoding="utf-8") as _output:
print("shouldBuild=%s" % should_build, file=_output)
print("sha=%s" % sha, file=_output)
print("version=%s" % version, file=_output)
print("prerelease=%s" % prerelease, file=_output)
print("imageTags=%s" % image_tags_rendered, file=_output)
print("imageMainTag=%s" % image_main_tag, file=_output)
print(f"shouldBuild={should_build}", file=_output)
print(f"sha={sha}", file=_output)
print(f"version={version}", file=_output)
print(f"prerelease={prerelease}", file=_output)
print(f"imageTags={image_tags_rendered}", file=_output)
print(f"imageMainTag={image_main_tag}", file=_output)

View File

@ -130,7 +130,7 @@ jobs:
- name: Setup authentik env
uses: ./.github/actions/setup
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.9.0
uses: helm/kind-action@v1.10.0
- name: run integration
run: |
poetry run coverage run manage.py test tests/integration

View File

@ -29,7 +29,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: golangci-lint
uses: golangci/golangci-lint-action@v4
uses: golangci/golangci-lint-action@v6
with:
version: v1.54.2
args: --timeout 5000s --verbose

View File

@ -34,6 +34,13 @@ jobs:
- name: Eslint
working-directory: ${{ matrix.project }}/
run: npm run lint
lint-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: web/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-build:
runs-on: ubuntu-latest
steps:
@ -95,6 +102,7 @@ jobs:
run: npm run lit-analyse
ci-web-mark:
needs:
- lint-lockfile
- lint-eslint
- lint-prettier
- lint-lit-analyse

View File

@ -12,6 +12,13 @@ on:
- version-*
jobs:
lint-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: website/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-prettier:
runs-on: ubuntu-latest
steps:
@ -62,6 +69,7 @@ jobs:
run: npm run ${{ matrix.job }}
ci-website-mark:
needs:
- lint-lockfile
- lint-prettier
- test
- build

View File

@ -155,8 +155,8 @@ jobs:
- uses: actions/checkout@v4
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis

View File

@ -14,8 +14,8 @@ jobs:
- uses: actions/checkout@v4
- name: Pre-release test
run: |
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
docker buildx install
mkdir -p ./gen-ts-api
docker build -t testing:latest .

View File

@ -1,7 +1,7 @@
# syntax=docker/dockerfile:1
# Stage 1: Build website
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as website-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as website-builder
ENV NODE_ENV=production
@ -20,7 +20,7 @@ COPY ./SECURITY.md /work/
RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/node:21 as web-builder
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as web-builder
ENV NODE_ENV=production
@ -38,7 +38,7 @@ COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.22.2-bookworm AS go-builder
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.22.3-bookworm AS go-builder
ARG TARGETOS
ARG TARGETARCH

View File

@ -19,6 +19,7 @@ pg_name := $(shell python -m authentik.lib.config postgresql.name 2>/dev/null)
CODESPELL_ARGS = -D - -D .github/codespell-dictionary.txt \
-I .github/codespell-words.txt \
-S 'web/src/locales/**' \
-S 'website/developer-docs/api/reference/**' \
authentik \
internal \
cmd \
@ -46,8 +47,8 @@ test-go:
go test -timeout 0 -v -race -cover ./...
test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(openssl rand -base64 32)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 32)" >> .env
echo "PG_PASS=$(shell openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis

View File

@ -2,7 +2,7 @@
from os import environ
__version__ = "2024.2.3"
__version__ = "2024.4.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -17,6 +17,7 @@ from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
@ -100,7 +101,10 @@ class GroupSerializer(ModelSerializer):
extra_kwargs = {
"users": {
"default": list,
}
},
# TODO: This field isn't unique on the database which is hard to backport
# hence we just validate the uniqueness here
"name": {"validators": [UniqueValidator(Group.objects.all())]},
}
@ -154,12 +158,18 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
pk = IntegerField(required=True)
queryset = Group.objects.all().select_related("parent").prefetch_related("users")
queryset = Group.objects.none()
serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter
ordering = ["name"]
def get_queryset(self):
base_qs = Group.objects.all().select_related("parent").prefetch_related("roles")
if self.serializer_class(context={"request": self.request})._should_include_users:
base_qs = base_qs.prefetch_related("users")
return base_qs
@extend_schema(
parameters=[
OpenApiParameter("include_users", bool, default=True),

View File

@ -2,6 +2,7 @@
from typing import Any
from django.utils.timezone import now
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiResponse, extend_schema, inline_serializer
from guardian.shortcuts import assign_perm, get_anonymous_user
@ -27,7 +28,6 @@ from authentik.core.models import (
TokenIntents,
User,
default_token_duration,
token_expires_from_timedelta,
)
from authentik.events.models import Event, EventAction
from authentik.events.utils import model_to_dict
@ -68,15 +68,17 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
max_token_lifetime_dt = default_token_duration()
if max_token_lifetime is not None:
try:
max_token_lifetime_dt = timedelta_from_string(max_token_lifetime)
max_token_lifetime_dt = now() + timedelta_from_string(max_token_lifetime)
except ValueError:
max_token_lifetime_dt = default_token_duration()
pass
if "expires" in attrs and attrs.get("expires") > token_expires_from_timedelta(
max_token_lifetime_dt
):
if "expires" in attrs and attrs.get("expires") > max_token_lifetime_dt:
raise ValidationError(
{"expires": f"Token expires exceeds maximum lifetime ({max_token_lifetime})."}
{
"expires": (
f"Token expires exceeds maximum lifetime ({max_token_lifetime_dt} UTC)."
)
}
)
elif attrs.get("intent") == TokenIntents.INTENT_API:
# For API tokens, expires cannot be overridden

View File

@ -407,8 +407,11 @@ class UserViewSet(UsedByMixin, ModelViewSet):
search_fields = ["username", "name", "is_active", "email", "uuid"]
filterset_class = UsersFilter
def get_queryset(self): # pragma: no cover
return User.objects.all().exclude_anonymous().prefetch_related("ak_groups")
def get_queryset(self):
base_qs = User.objects.all().exclude_anonymous()
if self.serializer_class(context={"request": self.request})._should_include_groups:
base_qs = base_qs.prefetch_related("ak_groups")
return base_qs
@extend_schema(
parameters=[

View File

@ -1,7 +0,0 @@
"""authentik core exceptions"""
from authentik.lib.sentry import SentryIgnoredException
class PropertyMappingExpressionException(SentryIgnoredException):
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""

View File

@ -6,6 +6,7 @@ from django.db.models import Model
from django.http import HttpRequest
from prometheus_client import Histogram
from authentik.core.expression.exceptions import SkipObjectException
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.expression.evaluator import BaseEvaluator
@ -47,6 +48,7 @@ class PropertyMappingEvaluator(BaseEvaluator):
self._context["request"] = req
req.context.update(**kwargs)
self._context.update(**kwargs)
self._globals["SkipObject"] = SkipObjectException
self.dry_run = dry_run
def handle_error(self, exc: Exception, expression_source: str):

View File

@ -0,0 +1,13 @@
"""authentik core exceptions"""
from authentik.lib.sentry import SentryIgnoredException
class PropertyMappingExpressionException(SentryIgnoredException):
"""Error when a PropertyMapping Exception expression could not be parsed or evaluated."""
class SkipObjectException(PropertyMappingExpressionException):
"""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"""

View File

@ -7,9 +7,10 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
def backport_is_backchannel(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
from authentik.core.models import BackchannelProvider
from authentik.providers.ldap.models import LDAPProvider
from authentik.providers.scim.models import SCIMProvider
for model in BackchannelProvider.__subclasses__():
for model in [LDAPProvider, SCIMProvider]:
try:
for obj in model.objects.only("is_backchannel"):
obj.is_backchannel = True

View File

@ -1,6 +1,6 @@
"""authentik core models"""
from datetime import datetime, timedelta
from datetime import datetime
from hashlib import sha256
from typing import Any, Optional, Self
from uuid import uuid4
@ -22,7 +22,7 @@ from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.blueprints.models import ManagedModel
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.generators import generate_id
@ -68,11 +68,6 @@ def default_token_duration() -> datetime:
return now() + timedelta_from_string(token_duration)
def token_expires_from_timedelta(dt: timedelta) -> datetime:
"""Return a `datetime.datetime` object with the duration of the Token"""
return now() + dt
def default_token_key() -> str:
"""Default token key"""
current_tenant = get_current_tenant()
@ -637,7 +632,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
raise NotImplementedError
def __str__(self) -> str:
return f"User-source connection (user={self.user.username}, source={self.source.slug})"
return f"User-source connection (user={self.user_id}, source={self.source_id})"
class Meta:
unique_together = (("user", "source"),)

View File

@ -100,8 +100,6 @@ class SourceFlowManager:
if self.request.user.is_authenticated:
new_connection.user = self.request.user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
existing_connections = self.connection_type.objects.filter(
@ -148,7 +146,6 @@ class SourceFlowManager:
]:
new_connection.user = user
new_connection = self.update_connection(new_connection, **kwargs)
new_connection.save()
return Action.LINK, new_connection
if self.source.user_matching_mode in [
SourceUserMatchingModes.EMAIL_DENY,

View File

@ -2,7 +2,9 @@
from datetime import datetime, timedelta
from django.conf import ImproperlyConfigured
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.contrib.sessions.backends.db import SessionStore as DBSessionStore
from django.core.cache import cache
from django.utils.timezone import now
from structlog.stdlib import get_logger
@ -15,6 +17,7 @@ from authentik.core.models import (
User,
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@ -39,16 +42,31 @@ def clean_expired_models(self: SystemTask):
amount = 0
for session in AuthenticatedSession.objects.all():
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
match CONFIG.get("session_storage", "cache"):
case "cache":
cache_key = f"{KEY_PREFIX}{session.session_key}"
value = None
try:
value = cache.get(cache_key)
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
except Exception as exc:
LOGGER.debug("Failed to get session from cache", exc=exc)
if not value:
session.delete()
amount += 1
case "db":
if not (
DBSessionStore.get_model_class()
.objects.filter(session_key=session.session_key, expire_date__gt=now())
.exists()
):
session.delete()
amount += 1
case _:
# Should never happen, as we check for other values in authentik/root/settings.py
raise ImproperlyConfigured(
"Invalid session_storage setting, allowed values are db and cache"
)
LOGGER.debug("Expired sessions", model=AuthenticatedSession, amount=amount)
messages.append(f"Expired {amount} {AuthenticatedSession._meta.verbose_name_plural}")

View File

@ -5,7 +5,7 @@ from guardian.shortcuts import assign_perm
from rest_framework.test import APITestCase
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_user
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.lib.generators import generate_id
@ -16,6 +16,13 @@ class TestGroupsAPI(APITestCase):
self.login_user = create_test_user()
self.user = User.objects.create(username="test-user")
def test_list_with_users(self):
"""Test listing with users"""
admin = create_test_admin_user()
self.client.force_login(admin)
response = self.client.get(reverse("authentik_api:group-list"), {"include_users": "true"})
self.assertEqual(response.status_code, 200)
def test_add_user(self):
"""Test add_user"""
group = Group.objects.create(name=generate_id())

View File

@ -3,7 +3,7 @@
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
@ -66,14 +66,11 @@ class TestPropertyMappings(TestCase):
expression="return request.http_request.path",
)
http_request = self.factory.get("/")
tmpl = (
"""
res = ak_call_policy('%s')
tmpl = f"""
res = ak_call_policy('{expr.name}')
result = [request.http_request.path, res.raw_result]
return result
"""
% expr.name
)
evaluator = PropertyMapping(expression=tmpl, name=generate_id())
res = evaluator.evaluate(self.user, http_request)
self.assertEqual(res, ["/", "/"])

View File

@ -48,15 +48,21 @@ class TestSourceFlowManager(TestCase):
def test_authenticated_link(self):
"""Test authenticated user linking"""
UserOAuthSourceConnection.objects.create(
user=get_anonymous_user(), source=self.source, identifier=self.identifier
)
user = User.objects.create(username="foo", email="foo@bar.baz")
flow_manager = OAuthSourceFlowManager(
self.source, get_request("/", user=user), self.identifier, {}
)
action, _ = flow_manager.get_action()
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
flow_manager.get_flow()
def test_unauthenticated_link(self):
"""Test un-authenticated user linking"""
flow_manager = OAuthSourceFlowManager(self.source, get_request("/"), self.identifier, {})
action, connection = flow_manager.get_action()
self.assertEqual(action, Action.LINK)
self.assertIsNone(connection.pk)
flow_manager.get_flow()
def test_unauthenticated_enroll_email(self):

View File

@ -41,6 +41,12 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 200)
def test_list_with_groups(self):
"""Test listing with groups"""
self.client.force_login(self.admin)
response = self.client.get(reverse("authentik_api:user-list"), {"include_groups": "true"})
self.assertEqual(response.status_code, 200)
def test_metrics(self):
"""Test user's metrics"""
self.client.force_login(self.admin)

View File

@ -8,7 +8,6 @@ from rest_framework.test import APITestCase
from authentik.core.models import User
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.config import CONFIG
from authentik.tenants.utils import get_current_tenant
@ -25,7 +24,6 @@ class TestUsersAvatars(APITestCase):
tenant.avatars = mode
tenant.save()
@CONFIG.patch("avatars", "none")
def test_avatars_none(self):
"""Test avatars none"""
self.set_avatar_mode("none")

View File

@ -4,7 +4,7 @@ from django.utils.text import slugify
from authentik.brands.models import Brand
from authentik.core.models import Group, User
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.flows.models import Flow, FlowDesignation
from authentik.lib.generators import generate_id
@ -50,12 +50,10 @@ def create_test_brand(**kwargs) -> Brand:
return Brand.objects.create(domain=uid, default=True, **kwargs)
def create_test_cert(use_ec_private_key=False) -> CertificateKeyPair:
def create_test_cert(alg=PrivateKeyAlg.RSA) -> CertificateKeyPair:
"""Generate a certificate for testing"""
builder = CertificateBuilder(
name=f"{generate_id()}.self-signed.goauthentik.io",
use_ec_private_key=use_ec_private_key,
)
builder = CertificateBuilder(f"{generate_id()}.self-signed.goauthentik.io")
builder.alg = alg
builder.build(
subject_alt_names=[f"{generate_id()}.self-signed.goauthentik.io"],
validity_days=360,

View File

@ -14,7 +14,13 @@ from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, DateTimeField, IntegerField, SerializerMethodField
from rest_framework.fields import (
CharField,
ChoiceField,
DateTimeField,
IntegerField,
SerializerMethodField,
)
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
@ -26,7 +32,7 @@ from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.rbac.decorators import permission_required
@ -178,6 +184,7 @@ class CertificateGenerationSerializer(PassiveSerializer):
common_name = CharField()
subject_alt_name = CharField(required=False, allow_blank=True, label=_("Subject-alt name"))
validity_days = IntegerField(initial=365)
alg = ChoiceField(default=PrivateKeyAlg.RSA, choices=PrivateKeyAlg.choices)
class CertificateKeyPairFilter(FilterSet):
@ -240,6 +247,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
raw_san = data.validated_data.get("subject_alt_name", "")
sans = raw_san.split(",") if raw_san != "" else []
builder = CertificateBuilder(data.validated_data["common_name"])
builder.alg = data.validated_data["alg"]
builder.build(
subject_alt_names=sans,
validity_days=int(data.validated_data["validity_days"]),

View File

@ -9,20 +9,28 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec, rsa
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
from cryptography.x509.oid import NameOID
from django.db import models
from django.utils.translation import gettext_lazy as _
from authentik import __version__
from authentik.crypto.models import CertificateKeyPair
class PrivateKeyAlg(models.TextChoices):
"""Algorithm to create private key with"""
RSA = "rsa", _("rsa")
ECDSA = "ecdsa", _("ecdsa")
class CertificateBuilder:
"""Build self-signed certificates"""
common_name: str
alg: PrivateKeyAlg
_use_ec_private_key: bool
def __init__(self, name: str, use_ec_private_key=False):
self._use_ec_private_key = use_ec_private_key
def __init__(self, name: str):
self.alg = PrivateKeyAlg.RSA
self.__public_key = None
self.__private_key = None
self.__builder = None
@ -42,11 +50,13 @@ class CertificateBuilder:
def generate_private_key(self) -> PrivateKeyTypes:
"""Generate private key"""
if self._use_ec_private_key:
if self.alg == PrivateKeyAlg.ECDSA:
return ec.generate_private_key(curve=ec.SECP256R1())
return rsa.generate_private_key(
public_exponent=65537, key_size=4096, backend=default_backend()
)
if self.alg == PrivateKeyAlg.RSA:
return rsa.generate_private_key(
public_exponent=65537, key_size=4096, backend=default_backend()
)
raise ValueError(f"Invalid alg: {self.alg}")
def build(
self,

View File

@ -2,11 +2,12 @@
from copy import deepcopy
from functools import partial
from typing import Any
from django.apps.registry import apps
from django.core.files import File
from django.db import connection
from django.db.models import Model
from django.db.models import ManyToManyRel, Model
from django.db.models.expressions import BaseExpression, Combinable
from django.db.models.signals import post_init
from django.http import HttpRequest
@ -44,7 +45,7 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
post_init.disconnect(dispatch_uid=request.request_id)
def serialize_simple(self, model: Model) -> dict:
"""Serialize a model in a very simple way. No ForeginKeys or other relationships are
"""Serialize a model in a very simple way. No ForeignKeys or other relationships are
resolved"""
data = {}
deferred_fields = model.get_deferred_fields()
@ -70,6 +71,9 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
for key, value in before.items():
if after.get(key) != value:
diff[key] = {"previous_value": value, "new_value": after.get(key)}
for key, value in after.items():
if key not in before and key not in diff and before.get(key) != value:
diff[key] = {"previous_value": before.get(key), "new_value": value}
return sanitize_item(diff)
def post_init_handler(self, request: HttpRequest, sender, instance: Model, **_):
@ -98,8 +102,37 @@ class EnterpriseAuditMiddleware(AuditMiddleware):
thread_kwargs = {}
if hasattr(instance, "_previous_state") or created:
prev_state = getattr(instance, "_previous_state", {})
if created:
prev_state = {}
# Get current state
new_state = self.serialize_simple(instance)
diff = self.diff(prev_state, new_state)
thread_kwargs["diff"] = diff
return super().post_save_handler(request, sender, instance, created, thread_kwargs, **_)
def m2m_changed_handler( # noqa: PLR0913
self,
request: HttpRequest,
sender,
instance: Model,
action: str,
pk_set: set[Any],
thread_kwargs: dict | None = None,
**_,
):
thread_kwargs = {}
m2m_field = None
# For the audit log we don't care about `pre_` or `post_` so we trim that part off
_, _, action_direction = action.partition("_")
# resolve the "through" model to an actual field
for field in instance._meta.get_fields():
if not isinstance(field, ManyToManyRel):
continue
if field.through == sender:
m2m_field = field
if m2m_field:
# If we're clearing we just set the "flag" to True
if action_direction == "clear":
pk_set = True
thread_kwargs["diff"] = {m2m_field.related_name: {action_direction: pk_set}}
return super().m2m_changed_handler(request, sender, instance, action, thread_kwargs)

View File

@ -1,9 +1,22 @@
from unittest.mock import PropertyMock, patch
from django.apps import apps
from django.conf import settings
from django.test import TestCase
from django.urls import reverse
from rest_framework.test import APITestCase
from authentik.core.models import Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
from authentik.events.utils import sanitize_item
from authentik.lib.generators import generate_id
class TestEnterpriseAudit(TestCase):
class TestEnterpriseAudit(APITestCase):
"""Test audit middleware"""
def setUp(self) -> None:
self.user = create_test_admin_user()
def test_import(self):
"""Ensure middleware is imported when app.ready is called"""
@ -16,3 +29,182 @@ class TestEnterpriseAudit(TestCase):
self.assertIn(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware", settings.MIDDLEWARE
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_create(self):
"""Test create audit log"""
self.client.force_login(self.user)
username = generate_id()
response = self.client.post(
reverse("authentik_api:user-list"),
data={"name": generate_id(), "username": username, "groups": [], "path": "foo"},
)
user = User.objects.get(username=username)
self.assertEqual(response.status_code, 201)
events = Event.objects.filter(
action=EventAction.MODEL_CREATED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{
"name": {
"new_value": user.name,
"previous_value": None,
},
"path": {"new_value": "foo", "previous_value": None},
"type": {"new_value": "internal", "previous_value": None},
"uuid": {
"new_value": user.uuid.hex,
"previous_value": None,
},
"email": {"new_value": "", "previous_value": None},
"username": {
"new_value": user.username,
"previous_value": None,
},
"is_active": {"new_value": True, "previous_value": None},
"attributes": {"new_value": {}, "previous_value": None},
"date_joined": {
"new_value": sanitize_item(user.date_joined),
"previous_value": None,
},
"first_name": {"new_value": "", "previous_value": None},
"id": {"new_value": user.pk, "previous_value": None},
"last_name": {"new_value": "", "previous_value": None},
"password": {"new_value": "********************", "previous_value": None},
"password_change_date": {
"new_value": sanitize_item(user.password_change_date),
"previous_value": None,
},
},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_update(self):
"""Test update audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
current_name = user.name
new_name = generate_id()
response = self.client.patch(
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
data={"name": new_name},
)
user.refresh_from_db()
self.assertEqual(response.status_code, 200)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{
"name": {
"new_value": new_name,
"previous_value": current_name,
},
},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_delete(self):
"""Test delete audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
response = self.client.delete(
reverse("authentik_api:user-detail", kwargs={"pk": user.id}),
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_DELETED,
context__model__model_name="user",
context__model__app="authentik_core",
context__model__pk=user.pk,
)
event = events.first()
self.assertIsNotNone(event)
self.assertNotIn("diff", event.context)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_m2m_add(self):
"""Test m2m add audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
response = self.client.post(
reverse("authentik_api:group-add-user", kwargs={"pk": group.group_uuid}),
data={
"pk": user.pk,
},
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="group",
context__model__app="authentik_core",
context__model__pk=group.pk.hex,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{"users": {"add": [user.pk]}},
)
@patch(
"authentik.enterprise.audit.middleware.EnterpriseAuditMiddleware.enabled",
PropertyMock(return_value=True),
)
def test_m2m_remove(self):
"""Test m2m remove audit log"""
self.client.force_login(self.user)
user = create_test_admin_user()
group = Group.objects.create(name=generate_id())
response = self.client.post(
reverse("authentik_api:group-remove-user", kwargs={"pk": group.group_uuid}),
data={
"pk": user.pk,
},
)
self.assertEqual(response.status_code, 204)
events = Event.objects.filter(
action=EventAction.MODEL_UPDATED,
context__model__model_name="group",
context__model__app="authentik_core",
context__model__pk=group.pk.hex,
)
event = events.first()
self.assertIsNotNone(event)
self.assertIsNotNone(event.context["diff"])
diff = event.context["diff"]
self.assertEqual(
diff,
{"users": {"remove": [user.pk]}},
)

View File

@ -0,0 +1,39 @@
"""google Property mappings API Views"""
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import extend_schema_field
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderMapping
class GoogleProviderMappingSerializer(PropertyMappingSerializer):
"""GoogleProviderMapping Serializer"""
class Meta:
model = GoogleWorkspaceProviderMapping
fields = PropertyMappingSerializer.Meta.fields
class GoogleProviderMappingFilter(FilterSet):
"""Filter for GoogleProviderMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = GoogleWorkspaceProviderMapping
fields = "__all__"
class GoogleProviderMappingViewSet(UsedByMixin, ModelViewSet):
"""GoogleProviderMapping Viewset"""
queryset = GoogleWorkspaceProviderMapping.objects.all()
serializer_class = GoogleProviderMappingSerializer
filterset_class = GoogleProviderMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,54 @@
"""Google Provider API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
class GoogleProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""GoogleProvider Serializer"""
class Meta:
model = GoogleWorkspaceProvider
fields = [
"pk",
"name",
"property_mappings",
"property_mappings_group",
"component",
"assigned_backchannel_application_slug",
"assigned_backchannel_application_name",
"verbose_name",
"verbose_name_plural",
"meta_model_name",
"delegated_subject",
"credentials",
"scopes",
"exclude_users_service_account",
"filter_group",
"user_delete_action",
"group_delete_action",
"default_group_email_domain",
]
extra_kwargs = {}
class GoogleProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
"""GoogleProvider Viewset"""
queryset = GoogleWorkspaceProvider.objects.all()
serializer_class = GoogleProviderSerializer
filterset_fields = [
"name",
"exclude_users_service_account",
"delegated_subject",
"filter_group",
]
search_fields = ["name"]
ordering = ["name"]
sync_single_task = google_workspace_sync

View File

@ -0,0 +1,9 @@
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderGoogleConfig(EnterpriseConfig):
name = "authentik.enterprise.providers.google_workspace"
label = "authentik_providers_google_workspace"
verbose_name = "authentik Enterprise.Providers.Google Workspace"
default = True

View File

@ -0,0 +1,71 @@
from django.db.models import Model
from django.http import HttpResponseNotFound
from google.auth.exceptions import GoogleAuthError, TransportError
from googleapiclient.discovery import build
from googleapiclient.errors import Error, HttpError
from googleapiclient.http import HttpRequest
from httplib2 import HttpLib2Error, HttpLib2ErrorWithResponse
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import (
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
TransientSyncException,
)
class GoogleWorkspaceSyncClient[TModel: Model, TConnection: Model, TSchema: dict](
BaseOutgoingSyncClient[TModel, TConnection, TSchema, GoogleWorkspaceProvider]
):
"""Base client for syncing to google workspace"""
domains: list
def __init__(self, provider: GoogleWorkspaceProvider) -> None:
super().__init__(provider)
self.directory_service = build(
"admin",
"directory_v1",
cache_discovery=False,
**provider.google_credentials(),
)
self.__prefetch_domains()
def __prefetch_domains(self):
self.domains = []
domains = self._request(self.directory_service.domains().list(customer="my_customer"))
for domain in domains.get("domains", []):
domain_name = domain.get("domainName")
self.domains.append(domain_name)
def _request(self, request: HttpRequest):
try:
response = request.execute()
except GoogleAuthError as exc:
if isinstance(exc, TransportError):
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
raise StopSync(exc) from exc
except HttpLib2Error as exc:
if isinstance(exc, HttpLib2ErrorWithResponse):
self._response_handle_status_code(exc.response.status, exc)
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
except HttpError as exc:
self._response_handle_status_code(exc.status_code, exc)
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
except Error as exc:
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
return response
def _response_handle_status_code(self, status_code: int, root_exc: Exception):
if status_code == HttpResponseNotFound.status_code:
raise NotFoundSyncException("Object not found") from root_exc
if status_code == HTTP_CONFLICT:
raise ObjectExistsSyncException("Object exists") from root_exc
def check_email_valid(self, *emails: str):
for email in emails:
if not any(email.endswith(f"@{domain_name}") for domain_name in self.domains):
raise TransientSyncException(f"Invalid email domain: {email}")

View File

@ -0,0 +1,245 @@
from deepmerge import always_merger
from django.db import transaction
from django.utils.text import slugify
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import Group
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceDeleteAction,
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderMapping,
GoogleWorkspaceProviderUser,
)
from authentik.events.models import Event, EventAction
from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.exceptions import (
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
TransientSyncException,
)
from authentik.lib.utils.errors import exception_to_string
class GoogleWorkspaceGroupClient(
GoogleWorkspaceSyncClient[Group, GoogleWorkspaceProviderGroup, dict]
):
"""Google client for groups"""
connection_type = GoogleWorkspaceProviderGroup
connection_type_query = "group"
can_discover = True
def to_schema(self, obj: Group) -> dict:
"""Convert authentik group"""
raw_google_group = {
"email": f"{slugify(obj.name)}@{self.provider.default_group_email_domain}"
}
for mapping in (
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
):
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
continue
try:
mapping: GoogleWorkspaceProviderMapping
value = mapping.evaluate(
user=None,
request=None,
group=obj,
provider=self.provider,
)
if value is None:
continue
always_merger.merge(raw_google_group, value)
except SkipObjectException as exc:
raise exc from exc
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
mapping=mapping,
).save()
raise StopSync(exc, obj, mapping) from exc
if not raw_google_group:
raise StopSync(ValueError("No group mappings configured"), obj)
return raw_google_group
def delete(self, obj: Group):
"""Delete group"""
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=obj
).first()
if not google_group:
self.logger.debug("Group does not exist in Google, skipping")
return None
with transaction.atomic():
if self.provider.group_delete_action == GoogleWorkspaceDeleteAction.DELETE:
self._request(
self.directory_service.groups().delete(groupKey=google_group.google_id)
)
google_group.delete()
def create(self, group: Group):
"""Create group from scratch and create a connection object"""
google_group = self.to_schema(group)
self.check_email_valid(google_group["email"])
with transaction.atomic():
try:
response = self._request(self.directory_service.groups().insert(body=google_group))
except ObjectExistsSyncException:
# group already exists in google workspace, so we can connect them manually
# for groups we need to fetch the group from google as we connect on
# ID and not group email
group_data = self._request(
self.directory_service.groups().get(groupKey=google_group["email"])
)
GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=group_data["id"]
)
else:
GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=response["id"]
)
def update(self, group: Group, connection: GoogleWorkspaceProviderGroup):
"""Update existing group"""
google_group = self.to_schema(group)
self.check_email_valid(google_group["email"])
try:
return self._request(
self.directory_service.groups().update(
groupKey=connection.google_id,
body=google_group,
)
)
except NotFoundSyncException:
# Resource missing is handled by self.write, which will re-create the group
raise
def write(self, obj: Group):
google_group, created = super().write(obj)
if created:
self.create_sync_members(obj, google_group)
return google_group
def create_sync_members(self, obj: Group, google_group: dict):
"""Sync all members after a group was created"""
users = list(obj.users.order_by("id").values_list("id", flat=True))
connections = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user__pk__in=users
)
for user in connections:
try:
self._request(
self.directory_service.members().insert(
groupKey=google_group["id"],
body={
"email": user.google_id,
},
)
)
except TransientSyncException:
continue
def update_group(self, group: Group, action: Direction, users_set: set[int]):
"""Update a groups members"""
if action == Direction.add:
return self._patch_add_users(group, users_set)
if action == Direction.remove:
return self._patch_remove_users(group, users_set)
def _patch(self, google_group_id: str, direction: Direction, members: list[str]):
for user in members:
try:
if direction == Direction.add:
self._request(
self.directory_service.members().insert(
groupKey=google_group_id, body={"email": user}
)
)
if direction == Direction.remove:
self._request(
self.directory_service.members().delete(
groupKey=google_group_id, memberKey=user
)
)
except ObjectExistsSyncException:
pass
except TransientSyncException:
raise
def _patch_add_users(self, group: Group, users_set: set[int]):
"""Add users in users_set to group"""
if len(users_set) < 1:
return
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
if not google_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
GoogleWorkspaceProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("google_id", flat=True)
)
if len(user_ids) < 1:
return
self._patch(google_group.google_id, Direction.add, user_ids)
def _patch_remove_users(self, group: Group, users_set: set[int]):
"""Remove users in users_set from group"""
if len(users_set) < 1:
return
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
if not google_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
GoogleWorkspaceProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("google_id", flat=True)
)
if len(user_ids) < 1:
return
self._patch(google_group.google_id, Direction.remove, user_ids)
def discover(self):
"""Iterate through all groups and connect them with authentik groups if possible"""
request = self.directory_service.groups().list(
customer="my_customer", maxResults=500, orderBy="email"
)
while request:
response = request.execute()
for group in response.get("groups", []):
self._discover_single_group(group)
request = self.directory_service.groups().list_next(
previous_request=request, previous_response=response
)
def _discover_single_group(self, group: dict):
"""handle discovery of a single group"""
google_name = group["name"]
google_id = group["id"]
matching_authentik_group = (
self.provider.get_object_qs(Group).filter(name=google_name).first()
)
if not matching_authentik_group:
return
GoogleWorkspaceProviderGroup.objects.get_or_create(
provider=self.provider,
group=matching_authentik_group,
google_id=google_id,
)

View File

@ -0,0 +1,41 @@
from json import dumps
from httplib2 import Response
class MockHTTP:
_recorded_requests = []
_responses = {}
def __init__(
self,
raise_on_unrecorded=True,
) -> None:
self._recorded_requests = []
self._responses = {}
self.raise_on_unrecorded = raise_on_unrecorded
def add_response(self, uri: str, body: str | dict = "", meta: dict | None = None, method="GET"):
if isinstance(body, dict):
body = dumps(body)
self._responses[(uri, method.upper())] = (body, meta or {"status": "200"})
def requests(self):
return self._recorded_requests
def request(
self,
uri,
method="GET",
body=None,
headers=None,
redirections=1,
connection_type=None,
):
key = (uri, method.upper())
self._recorded_requests.append((uri, method, body, headers))
if key not in self._responses and self.raise_on_unrecorded:
raise AssertionError(key)
body, meta = self._responses[key]
return Response(meta), body.encode("utf-8")

View File

@ -0,0 +1,141 @@
from deepmerge import always_merger
from django.db import transaction
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import User
from authentik.enterprise.providers.google_workspace.clients.base import GoogleWorkspaceSyncClient
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceDeleteAction,
GoogleWorkspaceProviderMapping,
GoogleWorkspaceProviderUser,
)
from authentik.events.models import Event, EventAction
from authentik.lib.sync.outgoing.exceptions import (
ObjectExistsSyncException,
StopSync,
TransientSyncException,
)
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_values
class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceProviderUser, dict]):
"""Sync authentik users into google workspace"""
connection_type = GoogleWorkspaceProviderUser
connection_type_query = "user"
can_discover = True
def to_schema(self, obj: User) -> dict:
"""Convert authentik user"""
raw_google_user = {}
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
if not isinstance(mapping, GoogleWorkspaceProviderMapping):
continue
try:
mapping: GoogleWorkspaceProviderMapping
value = mapping.evaluate(
user=obj,
request=None,
provider=self.provider,
)
if value is None:
continue
always_merger.merge(raw_google_user, value)
except SkipObjectException as exc:
raise exc from exc
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
EventAction.CONFIGURATION_ERROR,
message=f"Failed to evaluate property-mapping {exception_to_string(exc)}",
mapping=mapping,
).save()
raise StopSync(exc, obj, mapping) from exc
if not raw_google_user:
raise StopSync(ValueError("No user mappings configured"), obj)
if "primaryEmail" not in raw_google_user:
raw_google_user["primaryEmail"] = str(obj.email)
return delete_none_values(raw_google_user)
def delete(self, obj: User):
"""Delete user"""
google_user = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user=obj
).first()
if not google_user:
self.logger.debug("User does not exist in Google, skipping")
return None
with transaction.atomic():
response = None
if self.provider.user_delete_action == GoogleWorkspaceDeleteAction.DELETE:
response = self._request(
self.directory_service.users().delete(userKey=google_user.google_id)
)
elif self.provider.user_delete_action == GoogleWorkspaceDeleteAction.SUSPEND:
response = self._request(
self.directory_service.users().update(
userKey=google_user.google_id, body={"suspended": True}
)
)
google_user.delete()
return response
def create(self, user: User):
"""Create user from scratch and create a connection object"""
google_user = self.to_schema(user)
self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
)
with transaction.atomic():
try:
response = self._request(self.directory_service.users().insert(body=google_user))
except ObjectExistsSyncException:
# user already exists in google workspace, so we can connect them manually
GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=user.email
)
except TransientSyncException as exc:
raise exc
else:
GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=response["primaryEmail"]
)
def update(self, user: User, connection: GoogleWorkspaceProviderUser):
"""Update existing user"""
google_user = self.to_schema(user)
self.check_email_valid(
google_user["primaryEmail"], *[x["address"] for x in google_user.get("emails", [])]
)
self._request(
self.directory_service.users().update(userKey=connection.google_id, body=google_user)
)
def discover(self):
"""Iterate through all users and connect them with authentik users if possible"""
request = self.directory_service.users().list(
customer="my_customer", maxResults=500, orderBy="email"
)
while request:
response = request.execute()
for user in response.get("users", []):
self._discover_single_user(user)
request = self.directory_service.users().list_next(
previous_request=request, previous_response=response
)
def _discover_single_user(self, user: dict):
"""handle discovery of a single user"""
email = user["primaryEmail"]
matching_authentik_user = self.provider.get_object_qs(User).filter(email=email).first()
if not matching_authentik_user:
return
GoogleWorkspaceProviderUser.objects.get_or_create(
provider=self.provider,
user=matching_authentik_user,
google_id=email,
)

View File

@ -0,0 +1,167 @@
# Generated by Django 5.0.4 on 2024-05-07 16:03
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
("authentik_core", "0035_alter_group_options_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name="GoogleWorkspaceProviderMapping",
fields=[
(
"propertymapping_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Google Workspace Provider Mapping",
"verbose_name_plural": "Google Workspace Provider Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.CreateModel(
name="GoogleWorkspaceProvider",
fields=[
(
"provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.provider",
),
),
("delegated_subject", models.EmailField(max_length=254)),
("credentials", models.JSONField()),
(
"scopes",
models.TextField(
default="https://www.googleapis.com/auth/admin.directory.user,https://www.googleapis.com/auth/admin.directory.group,https://www.googleapis.com/auth/admin.directory.group.member,https://www.googleapis.com/auth/admin.directory.domain.readonly"
),
),
("default_group_email_domain", models.TextField()),
("exclude_users_service_account", models.BooleanField(default=False)),
(
"user_delete_action",
models.TextField(
choices=[
("do_nothing", "Do Nothing"),
("delete", "Delete"),
("suspend", "Suspend"),
],
default="delete",
),
),
(
"group_delete_action",
models.TextField(
choices=[
("do_nothing", "Do Nothing"),
("delete", "Delete"),
("suspend", "Suspend"),
],
default="delete",
),
),
(
"filter_group",
models.ForeignKey(
default=None,
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.group",
),
),
(
"property_mappings_group",
models.ManyToManyField(
blank=True,
default=None,
help_text="Property mappings used for group creation/updating.",
to="authentik_core.propertymapping",
),
),
],
options={
"verbose_name": "Google Workspace Provider",
"verbose_name_plural": "Google Workspace Providers",
},
bases=("authentik_core.provider", models.Model),
),
migrations.CreateModel(
name="GoogleWorkspaceProviderGroup",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("google_id", models.TextField()),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to="authentik_core.group"
),
),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_google_workspace.googleworkspaceprovider",
),
),
],
options={
"unique_together": {("google_id", "group", "provider")},
},
),
migrations.CreateModel(
name="GoogleWorkspaceProviderUser",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("google_id", models.TextField()),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_google_workspace.googleworkspaceprovider",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"unique_together": {("google_id", "user", "provider")},
},
),
]

View File

@ -0,0 +1,179 @@
"""Google workspace sync provider"""
from typing import Any, Self
from uuid import uuid4
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from google.oauth2.service_account import Credentials
from rest_framework.serializers import Serializer
from authentik.core.models import (
BackchannelProvider,
Group,
PropertyMapping,
User,
UserTypes,
)
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
def default_scopes() -> list[str]:
return [
"https://www.googleapis.com/auth/admin.directory.user",
"https://www.googleapis.com/auth/admin.directory.group",
"https://www.googleapis.com/auth/admin.directory.group.member",
"https://www.googleapis.com/auth/admin.directory.domain.readonly",
]
class GoogleWorkspaceDeleteAction(models.TextChoices):
"""Action taken when a user/group is deleted in authentik. Suspend is not available for groups,
and will be treated as `do_nothing`"""
DO_NOTHING = "do_nothing"
DELETE = "delete"
SUSPEND = "suspend"
class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
"""Sync users from authentik into Google Workspace."""
delegated_subject = models.EmailField()
credentials = models.JSONField()
scopes = models.TextField(default=",".join(default_scopes()))
default_group_email_domain = models.TextField()
exclude_users_service_account = models.BooleanField(default=False)
user_delete_action = models.TextField(
choices=GoogleWorkspaceDeleteAction.choices, default=GoogleWorkspaceDeleteAction.DELETE
)
group_delete_action = models.TextField(
choices=GoogleWorkspaceDeleteAction.choices, default=GoogleWorkspaceDeleteAction.DELETE
)
filter_group = models.ForeignKey(
"authentik_core.group", on_delete=models.SET_DEFAULT, default=None, null=True
)
property_mappings_group = models.ManyToManyField(
PropertyMapping,
default=None,
blank=True,
help_text=_("Property mappings used for group creation/updating."),
)
def client_for_model(
self, model: type[User | Group]
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User):
from authentik.enterprise.providers.google_workspace.clients.users import (
GoogleWorkspaceUserClient,
)
return GoogleWorkspaceUserClient(self)
if issubclass(model, Group):
from authentik.enterprise.providers.google_workspace.clients.groups import (
GoogleWorkspaceGroupClient,
)
return GoogleWorkspaceGroupClient(self)
raise ValueError(f"Invalid model {model}")
def get_object_qs(self, type: type[User | Group]) -> QuerySet[User | Group]:
if type == User:
# Get queryset of all users with consistent ordering
# according to the provider's settings
base = User.objects.all().exclude_anonymous()
if self.exclude_users_service_account:
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
)
if self.filter_group:
base = base.filter(ak_groups__in=[self.filter_group])
return base.order_by("pk")
if type == Group:
# Get queryset of all groups with consistent ordering
return Group.objects.all().order_by("pk")
raise ValueError(f"Invalid type {type}")
def google_credentials(self):
return {
"credentials": Credentials.from_service_account_info(
self.credentials, scopes=self.scopes.split(",")
).with_subject(self.delegated_subject),
}
@property
def component(self) -> str:
return "ak-provider-google-workspace-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.providers import (
GoogleProviderSerializer,
)
return GoogleProviderSerializer
def __str__(self):
return f"Google Workspace Provider {self.name}"
class Meta:
verbose_name = _("Google Workspace Provider")
verbose_name_plural = _("Google Workspace Providers")
class GoogleWorkspaceProviderMapping(PropertyMapping):
"""Map authentik data to outgoing Google requests"""
@property
def component(self) -> str:
return "ak-property-mapping-google-workspace-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
GoogleProviderMappingSerializer,
)
return GoogleProviderMappingSerializer
def __str__(self):
return f"Google Workspace Provider Mapping {self.name}"
class Meta:
verbose_name = _("Google Workspace Provider Mapping")
verbose_name_plural = _("Google Workspace Provider Mappings")
class GoogleWorkspaceProviderUser(models.Model):
"""Mapping of a user and provider to a Google user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("google_id", "user", "provider"),)
def __str__(self) -> str:
return f"Google Workspace User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(models.Model):
"""Mapping of a group and provider to a Google group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("google_id", "group", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Group {self.group_id} to {self.provider_id}"

View File

@ -0,0 +1,13 @@
"""Google workspace provider task Settings"""
from celery.schedules import crontab
from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"providers_google_workspace_sync": {
"task": "authentik.enterprise.providers.google_workspace.tasks.google_workspace_sync_all",
"schedule": crontab(minute=fqdn_rand("google_workspace_sync_all"), hour="*/4"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -0,0 +1,16 @@
"""Google provider signals"""
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.enterprise.providers.google_workspace.tasks import (
google_workspace_sync,
google_workspace_sync_direct,
google_workspace_sync_m2m,
)
from authentik.lib.sync.outgoing.signals import register_signals
register_signals(
GoogleWorkspaceProvider,
task_sync_single=google_workspace_sync,
task_sync_direct=google_workspace_sync_direct,
task_sync_m2m=google_workspace_sync_m2m,
)

View File

@ -0,0 +1,34 @@
"""Google Provider tasks"""
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.events.system_tasks import SystemTask
from authentik.lib.sync.outgoing.tasks import SyncTasks
from authentik.root.celery import CELERY_APP
sync_tasks = SyncTasks(GoogleWorkspaceProvider)
@CELERY_APP.task()
def google_workspace_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@CELERY_APP.task(base=SystemTask, bind=True)
def google_workspace_sync(self, provider_pk: int, *args, **kwargs):
"""Run full sync for Google Workspace provider"""
return sync_tasks.sync_single(self, provider_pk, google_workspace_sync_objects)
@CELERY_APP.task()
def google_workspace_sync_all():
return sync_tasks.sync_all(google_workspace_sync)
@CELERY_APP.task()
def google_workspace_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@CELERY_APP.task()
def google_workspace_sync_m2m(*args, **kwargs):
return sync_tasks.sync_signal_m2m(*args, **kwargs)

View File

@ -0,0 +1,14 @@
{
"kind": "admin#directory#domains",
"etag": "\"a1kA7zE2sFLsHiFwgXN9G3effoc9grR2OwUu8_95xD4/uvC5HsKHylhnUtnRV6ZxINODtV0\"",
"domains": [
{
"kind": "admin#directory#domain",
"etag": "\"a1kA7zE2sFLsHiFwgXN9G3effoc9grR2OwUu8_95xD4/V4koSPWBFIWuIpAmUamO96QhTLo\"",
"domainName": "goauthentik.io",
"isPrimary": true,
"verified": true,
"creationTime": "1543048869840"
}
]
}

View File

@ -0,0 +1,313 @@
"""Google Workspace Group tests"""
from unittest.mock import MagicMock, patch
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.core.tests.utils import create_test_user
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceDeleteAction,
GoogleWorkspaceProvider,
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderMapping,
)
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.tenants.models import Tenant
domains_list_v1_mock = load_fixture("fixtures/domains_list_v1.json")
class GoogleWorkspaceGroupTests(TestCase):
"""Google workspace Group tests"""
@apply_blueprint("system/providers-google-workspace.yaml")
def setUp(self) -> None:
# Delete all groups and groups as the mocked HTTP responses only return one ID
# which will cause errors with multiple groups
Tenant.objects.update(avatars="none")
User.objects.all().exclude_anonymous().delete()
Group.objects.all().delete()
self.provider: GoogleWorkspaceProvider = GoogleWorkspaceProvider.objects.create(
name=generate_id(),
credentials={},
delegated_subject="",
exclude_users_service_account=True,
default_group_email_domain="goauthentik.io",
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
GoogleWorkspaceProviderMapping.objects.get(
managed="goauthentik.io/providers/google_workspace/user"
)
)
self.provider.property_mappings_group.add(
GoogleWorkspaceProviderMapping.objects.get(
managed="goauthentik.io/providers/google_workspace/group"
)
)
self.api_key = generate_id()
def test_group_create(self):
"""Test group creation"""
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?key={self.api_key}&alt=json",
method="POST",
body={"id": generate_id()},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
group = Group.objects.create(name=uid)
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(google_group)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 2)
def test_group_create_update(self):
"""Test group updating"""
uid = generate_id()
ext_id = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?key={self.api_key}&alt=json",
method="POST",
body={"id": ext_id},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups/{ext_id}?key={self.api_key}&alt=json",
method="PUT",
body={"id": ext_id},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
group = Group.objects.create(name=uid)
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(google_group)
group.name = "new name"
group.save()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 4)
def test_group_create_delete(self):
"""Test group deletion"""
uid = generate_id()
ext_id = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?key={self.api_key}&alt=json",
method="POST",
body={"id": ext_id},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups/{ext_id}?key={self.api_key}",
method="DELETE",
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
group = Group.objects.create(name=uid)
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(google_group)
group.delete()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 4)
def test_group_create_member_add(self):
"""Test group creation"""
uid = generate_id()
ext_id = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?key={self.api_key}&alt=json",
method="POST",
body={"id": ext_id},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}&alt=json",
method="PUT",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups/{ext_id}/members?key={self.api_key}&alt=json",
method="POST",
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = create_test_user(uid)
group = Group.objects.create(name=uid)
group.users.add(user)
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(google_group)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 8)
def test_group_create_member_remove(self):
"""Test group creation"""
uid = generate_id()
ext_id = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?key={self.api_key}&alt=json",
method="POST",
body={"id": ext_id},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}&alt=json",
method="PUT",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups/{ext_id}/members/{uid}%40goauthentik.io?key={self.api_key}",
method="DELETE",
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups/{ext_id}/members?key={self.api_key}&alt=json",
method="POST",
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = create_test_user(uid)
group = Group.objects.create(name=uid)
group.users.add(user)
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(google_group)
group.users.remove(user)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 10)
def test_group_create_delete_do_nothing(self):
"""Test group deletion (delete action = do nothing)"""
self.provider.group_delete_action = GoogleWorkspaceDeleteAction.DO_NOTHING
self.provider.save()
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?key={self.api_key}&alt=json",
method="POST",
body={"id": uid},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
group = Group.objects.create(name=uid)
google_group = GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(google_group)
group.delete()
self.assertEqual(len(http.requests()), 3)
self.assertFalse(
GoogleWorkspaceProviderGroup.objects.filter(
provider=self.provider, group__name=uid
).exists()
)
def test_sync_task(self):
"""Test group discovery"""
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
method="GET",
body={"users": []},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
method="GET",
body={"groups": [{"id": uid, "name": uid}]},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups/{uid}?key={self.api_key}&alt=json",
method="PUT",
body={"id": uid},
)
self.app.backchannel_providers.remove(self.provider)
different_group = Group.objects.create(
name=uid,
)
self.app.backchannel_providers.add(self.provider)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
google_workspace_sync.delay(self.provider.pk).get()
self.assertTrue(
GoogleWorkspaceProviderGroup.objects.filter(
group=different_group, provider=self.provider
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 5)

View File

@ -0,0 +1,287 @@
"""Google Workspace User tests"""
from json import loads
from unittest.mock import MagicMock, patch
from django.test import TestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.enterprise.providers.google_workspace.clients.test_http import MockHTTP
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceDeleteAction,
GoogleWorkspaceProvider,
GoogleWorkspaceProviderMapping,
GoogleWorkspaceProviderUser,
)
from authentik.enterprise.providers.google_workspace.tasks import google_workspace_sync
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.tenants.models import Tenant
domains_list_v1_mock = load_fixture("fixtures/domains_list_v1.json")
class GoogleWorkspaceUserTests(TestCase):
"""Google workspace User tests"""
@apply_blueprint("system/providers-google-workspace.yaml")
def setUp(self) -> None:
# Delete all users and groups as the mocked HTTP responses only return one ID
# which will cause errors with multiple users
Tenant.objects.update(avatars="none")
User.objects.all().exclude_anonymous().delete()
Group.objects.all().delete()
self.provider: GoogleWorkspaceProvider = GoogleWorkspaceProvider.objects.create(
name=generate_id(),
credentials={},
delegated_subject="",
exclude_users_service_account=True,
default_group_email_domain="goauthentik.io",
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
GoogleWorkspaceProviderMapping.objects.get(
managed="goauthentik.io/providers/google_workspace/user"
)
)
self.provider.property_mappings_group.add(
GoogleWorkspaceProviderMapping.objects.get(
managed="goauthentik.io/providers/google_workspace/group"
)
)
self.api_key = generate_id()
def test_user_create(self):
"""Test user creation"""
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
google_user = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(google_user)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 2)
def test_user_create_update(self):
"""Test user updating"""
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}&alt=json",
method="PUT",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
google_user = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(google_user)
user.name = "new name"
user.save()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 4)
def test_user_create_delete(self):
"""Test user deletion"""
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}",
method="DELETE",
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
google_user = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(google_user)
user.delete()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 4)
def test_user_create_delete_suspend(self):
"""Test user deletion (delete action = Suspend)"""
self.provider.user_delete_action = GoogleWorkspaceDeleteAction.SUSPEND
self.provider.save()
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}&alt=json",
method="PUT",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
google_user = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(google_user)
user.delete()
self.assertEqual(len(http.requests()), 4)
_, _, body, _ = http.requests()[3]
self.assertEqual(
loads(body),
{
"suspended": True,
},
)
self.assertFalse(
GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user__username=uid
).exists()
)
def test_user_create_delete_do_nothing(self):
"""Test user deletion (delete action = do nothing)"""
self.provider.user_delete_action = GoogleWorkspaceDeleteAction.DO_NOTHING
self.provider.save()
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?key={self.api_key}&alt=json",
method="POST",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
google_user = GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(google_user)
user.delete()
self.assertEqual(len(http.requests()), 3)
self.assertFalse(
GoogleWorkspaceProviderUser.objects.filter(
provider=self.provider, user__username=uid
).exists()
)
def test_sync_task(self):
"""Test user discovery"""
uid = generate_id()
http = MockHTTP()
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/customer/my_customer/domains?key={self.api_key}&alt=json",
domains_list_v1_mock,
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
method="GET",
body={"users": [{"primaryEmail": f"{uid}@goauthentik.io"}]},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/groups?customer=my_customer&maxResults=500&orderBy=email&key={self.api_key}&alt=json",
method="GET",
body={"groups": []},
)
http.add_response(
f"https://admin.googleapis.com/admin/directory/v1/users/{uid}%40goauthentik.io?key={self.api_key}&alt=json",
method="PUT",
body={"primaryEmail": f"{uid}@goauthentik.io"},
)
self.app.backchannel_providers.remove(self.provider)
different_user = User.objects.create(
username=uid,
email=f"{uid}@goauthentik.io",
)
self.app.backchannel_providers.add(self.provider)
with patch(
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceProvider.google_credentials",
MagicMock(return_value={"developerKey": self.api_key, "http": http}),
):
google_workspace_sync.delay(self.provider.pk).get()
self.assertTrue(
GoogleWorkspaceProviderUser.objects.filter(
user=different_user, provider=self.provider
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
self.assertEqual(len(http.requests()), 5)

View File

@ -0,0 +1,11 @@
"""google provider urls"""
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
GoogleProviderMappingViewSet,
)
from authentik.enterprise.providers.google_workspace.api.providers import GoogleProviderViewSet
api_urlpatterns = [
("providers/google_workspace", GoogleProviderViewSet),
("propertymappings/provider/google_workspace", GoogleProviderMappingViewSet),
]

View File

@ -11,7 +11,7 @@ from django.utils.translation import gettext as _
from rest_framework.serializers import Serializer
from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User, default_token_key
from authentik.events.models import Event, EventAction
from authentik.lib.models import SerializerModel
@ -201,10 +201,7 @@ class ConnectionToken(ExpiringModel):
return settings
def __str__(self):
return (
f"RAC Connection token {self.session.user} to "
f"{self.endpoint.provider.name}/{self.endpoint.name}"
)
return f"RAC Connection token {self.session_id} to {self.provider_id}/{self.endpoint_id}"
class Meta:
verbose_name = _("RAC Connection token")

View File

@ -14,6 +14,7 @@ CELERY_BEAT_SCHEDULE = {
TENANT_APPS = [
"authentik.enterprise.audit",
"authentik.enterprise.providers.google_workspace",
"authentik.enterprise.providers.rac",
"authentik.enterprise.stages.source",
]

View File

@ -60,6 +60,8 @@ class SystemTaskSerializer(ModelSerializer):
"duration",
"status",
"messages",
"expires",
"expiring",
]

View File

@ -116,12 +116,12 @@ class AuditMiddleware:
return user
user = getattr(request, "user", self.anonymous_user)
if not user.is_authenticated:
self._ensure_fallback_user()
return self.anonymous_user
return user
def connect(self, request: HttpRequest):
"""Connect signal for automatic logging"""
self._ensure_fallback_user()
if not hasattr(request, "request_id"):
return
post_save.connect(
@ -214,7 +214,15 @@ class AuditMiddleware:
model=model_to_dict(instance),
).run()
def m2m_changed_handler(self, request: HttpRequest, sender, instance: Model, action: str, **_):
def m2m_changed_handler(
self,
request: HttpRequest,
sender,
instance: Model,
action: str,
thread_kwargs: dict | None = None,
**_,
):
"""Signal handler for all object's m2m_changed"""
if action not in ["pre_add", "pre_remove", "post_clear"]:
return
@ -229,4 +237,5 @@ class AuditMiddleware:
request,
user=user,
model=model_to_dict(instance),
**thread_kwargs,
).run()

View File

@ -556,7 +556,7 @@ class Notification(SerializerModel):
if len(self.body) > NOTIFICATION_SUMMARY_LENGTH
else self.body
)
return f"Notification for user {self.user}: {body_trunc}"
return f"Notification for user {self.user_id}: {body_trunc}"
class Meta:
verbose_name = _("Notification")

View File

@ -6,7 +6,7 @@ from typing import Any
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from structlog.stdlib import BoundLogger, get_logger
from tenant_schemas_celery.task import TenantTask
from authentik.events.logs import LogEvent
@ -15,12 +15,12 @@ from authentik.events.models import SystemTask as DBSystemTask
from authentik.events.utils import sanitize_item
from authentik.lib.utils.errors import exception_to_string
LOGGER = get_logger()
class SystemTask(TenantTask):
"""Task which can save its state to the cache"""
logger: BoundLogger
# For tasks that should only be listed if they failed, set this to False
save_on_success: bool
@ -63,6 +63,7 @@ class SystemTask(TenantTask):
def before_start(self, task_id, args, kwargs):
self._start_precise = perf_counter()
self._start = now()
self.logger = get_logger().bind(task_id=task_id)
return super().before_start(task_id, args, kwargs)
def db(self) -> DBSystemTask | None:
@ -119,7 +120,7 @@ class SystemTask(TenantTask):
"task_call_kwargs": sanitize_item(kwargs),
"status": self._status,
"messages": sanitize_item(self._messages),
"expires": now() + timedelta(hours=self.result_timeout_hours),
"expires": now() + timedelta(hours=self.result_timeout_hours + 3),
"expiring": True,
},
)

View File

@ -4,7 +4,7 @@ from django.db.models.query_utils import Q
from guardian.shortcuts import get_anonymous_user
from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import User
from authentik.events.models import (
Event,

View File

@ -0,0 +1,35 @@
"""authentik event models tests"""
from collections.abc import Callable
from django.db.models import Model
from django.test import TestCase
from authentik.core.models import default_token_key
from authentik.lib.utils.reflection import get_apps
class TestModels(TestCase):
"""Test Models"""
def model_tester_factory(test_model: type[Model]) -> Callable:
"""Test models' __str__ and __repr__"""
def tester(self: TestModels):
allowed = 0
# Token-like objects need to lookup the current tenant to get the default token length
for field in test_model._meta.fields:
if field.default == default_token_key:
allowed += 1
with self.assertNumQueries(allowed):
str(test_model())
with self.assertNumQueries(allowed):
repr(test_model())
return tester
for app in get_apps():
for model in app.get_models():
setattr(TestModels, f"test_{app.label}_{model.__name__}", model_tester_factory(model))

View File

@ -278,7 +278,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
},
)
@action(detail=True, pagination_class=None, filter_backends=[])
def execute(self, request: Request, _slug: str):
def execute(self, request: Request, slug: str):
"""Execute flow for current user"""
# Because we pre-plan the flow here, and not in the planner, we need to manually clear
# the history of the inspector

View File

@ -6,6 +6,7 @@ from rest_framework.test import APITestCase
from authentik.core.tests.utils import create_test_admin_user
from authentik.flows.api.stages import StageSerializer, StageViewSet
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage
from authentik.lib.generators import generate_id
from authentik.policies.dummy.models import DummyPolicy
from authentik.policies.models import PolicyBinding
from authentik.stages.dummy.models import DummyStage
@ -101,3 +102,21 @@ class TestFlowsAPI(APITestCase):
reverse("authentik_api:stage-types"),
)
self.assertEqual(response.status_code, 200)
def test_execute(self):
"""Test execute endpoint"""
user = create_test_admin_user()
self.client.force_login(user)
flow = Flow.objects.create(
name=generate_id(),
slug=generate_id(),
designation=FlowDesignation.AUTHENTICATION,
)
FlowStageBinding.objects.create(
target=flow, stage=DummyStage.objects.create(name=generate_id()), order=0
)
response = self.client.get(
reverse("authentik_api:flow-execute", kwargs={"slug": flow.slug})
)
self.assertEqual(response.status_code, 200)

View File

@ -53,6 +53,7 @@ cache:
# result_backend:
# url: ""
# transport_options: ""
debug: false
remote_debug: false

View File

@ -9,6 +9,7 @@ from typing import Any
from cachetools import TLRUCache, cached
from django.core.exceptions import FieldError
from django.utils.text import slugify
from guardian.shortcuts import get_anonymous_user
from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub
@ -56,6 +57,7 @@ class BaseEvaluator:
"requests": get_http_session(),
"resolve_dns": BaseEvaluator.expr_resolve_dns,
"reverse_dns": BaseEvaluator.expr_reverse_dns,
"slugify": slugify,
}
self._context = {}

View File

@ -100,6 +100,7 @@ def get_logger_config():
"fsevents": "WARNING",
"uvicorn": "WARNING",
"gunicorn": "INFO",
"requests_mock": "WARNING",
}
for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = {

View File

View File

@ -0,0 +1,5 @@
"""Sync constants"""
PAGE_SIZE = 100
PAGE_TIMEOUT = 60 * 60 * 0.5 # Half an hour
HTTP_CONFLICT = 409

View File

@ -0,0 +1,54 @@
from collections.abc import Callable
from django.utils.text import slugify
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import BooleanField
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
class SyncStatusSerializer(PassiveSerializer):
"""Provider sync status"""
is_running = BooleanField(read_only=True)
tasks = SystemTaskSerializer(many=True, read_only=True)
class OutgoingSyncProviderStatusMixin:
"""Common API Endpoints for Outgoing sync providers"""
sync_single_task: Callable = None
@extend_schema(
responses={
200: SyncStatusSerializer(),
404: OpenApiResponse(description="Task not found"),
}
)
@action(
methods=["GET"],
detail=True,
pagination_class=None,
url_path="sync/status",
filter_backends=[],
)
def sync_status(self, request: Request, pk: int) -> Response:
"""Get provider's sync status"""
provider: OutgoingSyncProvider = self.get_object()
tasks = list(
get_objects_for_user(request.user, "authentik_events.view_systemtask").filter(
name=self.sync_single_task.__name__,
uid=slugify(provider.name),
)
)
status = {
"tasks": tasks,
"is_running": provider.sync_lock.locked(),
}
return Response(SyncStatusSerializer(status).data)

View File

@ -0,0 +1,83 @@
"""Basic outgoing sync Client"""
from enum import StrEnum
from typing import TYPE_CHECKING
from django.db import DatabaseError
from structlog.stdlib import get_logger
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException
if TYPE_CHECKING:
from django.db.models import Model
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
class Direction(StrEnum):
add = "add"
remove = "remove"
class BaseOutgoingSyncClient[
TModel: "Model", TConnection: "Model", TSchema: dict, TProvider: "OutgoingSyncProvider"
]:
"""Basic Outgoing sync client Client"""
provider: TProvider
connection_type: type[TConnection]
connection_type_query: str
can_discover = False
def __init__(self, provider: TProvider):
self.logger = get_logger().bind(provider=provider.name)
self.provider = provider
def create(self, obj: TModel) -> TConnection:
"""Create object in remote destination"""
raise NotImplementedError()
def update(self, obj: TModel, connection: object):
"""Update object in remote destination"""
raise NotImplementedError()
def write(self, obj: TModel) -> tuple[TConnection, bool]:
"""Write object to destination. Uses self.create and self.update, but
can be overwritten for further logic"""
remote_obj = self.connection_type.objects.filter(
provider=self.provider, **{self.connection_type_query: obj}
).first()
connection: TConnection | None = None
try:
if not remote_obj:
connection = self.create(obj)
return connection, True
try:
self.update(obj, remote_obj)
return remote_obj, False
except NotFoundSyncException:
remote_obj.delete()
connection = self.create(obj)
return connection, True
except DatabaseError as exc:
self.logger.warning("Failed to write object", obj=obj, exc=exc)
if connection:
connection.delete()
return None, False
def delete(self, obj: TModel):
"""Delete object from destination"""
raise NotImplementedError()
def to_schema(self, obj: TModel) -> TSchema:
"""Convert object to destination schema"""
raise NotImplementedError()
def discover(self):
"""Optional method. Can be used to implement a "discovery" where
upon creation of this provider, this function will be called and can
pre-link any users/groups in the remote system with the respective
object in authentik based on a common identifier"""
raise NotImplementedError()

View File

@ -0,0 +1,37 @@
from authentik.lib.sentry import SentryIgnoredException
class BaseSyncException(SentryIgnoredException):
"""Base class for all sync exceptions"""
class TransientSyncException(BaseSyncException):
"""Transient sync exception which may be caused by network blips, etc"""
class NotFoundSyncException(BaseSyncException):
"""Exception when an object was not found in the remote system"""
class ObjectExistsSyncException(BaseSyncException):
"""Exception when an object already exists in the remote system"""
class StopSync(BaseSyncException):
"""Exception raised when a configuration error should stop the sync process"""
def __init__(
self, exc: Exception, obj: object | None = None, mapping: object | None = None
) -> None:
self.exc = exc
self.obj = obj
self.mapping = mapping
def detail(self) -> str:
"""Get human readable details of this error"""
msg = f"Error {str(self.exc)}"
if self.obj:
msg += f", caused by {self.obj}"
if self.mapping:
msg += f" (mapping {self.mapping})"
return msg

View File

@ -0,0 +1,32 @@
from typing import Any, Self
from django.core.cache import cache
from django.db.models import Model, QuerySet
from redis.lock import Lock
from authentik.core.models import Group, User
from authentik.lib.sync.outgoing import PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
class OutgoingSyncProvider(Model):
class Meta:
abstract = True
def client_for_model[
T: User | Group
](self, model: type[T]) -> BaseOutgoingSyncClient[T, Any, Any, Self]:
raise NotImplementedError
def get_object_qs[T: User | Group](self, type: type[T]) -> QuerySet[T]:
raise NotImplementedError
@property
def sync_lock(self) -> Lock:
"""Redis lock to prevent multiple parallel syncs happening"""
return Lock(
cache.client.get_client(),
name=f"goauthentik.io/providers/outgoing-sync/{str(self.pk)}",
timeout=(60 * 60 * PAGE_TIMEOUT) * 3,
)

View File

@ -0,0 +1,71 @@
from collections.abc import Callable
from django.core.paginator import Paginator
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete
from authentik.core.models import Group, User
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.reflection import class_to_path
def register_signals(
provider_type: type[OutgoingSyncProvider],
task_sync_single: Callable[[int], None],
task_sync_direct: Callable[[int], None],
task_sync_m2m: Callable[[int], None],
):
"""Register sync signals"""
uid = class_to_path(provider_type)
def post_save_provider(sender: type[Model], instance: OutgoingSyncProvider, created: bool, **_):
"""Trigger sync when Provider is saved"""
users_paginator = Paginator(instance.get_object_qs(User), PAGE_SIZE)
groups_paginator = Paginator(instance.get_object_qs(Group), PAGE_SIZE)
soft_time_limit = (users_paginator.num_pages + groups_paginator.num_pages) * PAGE_TIMEOUT
time_limit = soft_time_limit * 1.5
task_sync_single.apply_async(
(instance.pk,), time_limit=int(time_limit), soft_time_limit=int(soft_time_limit)
)
post_save.connect(post_save_provider, provider_type, dispatch_uid=uid, weak=False)
def model_post_save(sender: type[Model], instance: User | Group, created: bool, **_):
"""Post save handler"""
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
return
task_sync_direct.delay(class_to_path(instance.__class__), instance.pk, Direction.add.value)
post_save.connect(model_post_save, User, dispatch_uid=uid, weak=False)
post_save.connect(model_post_save, Group, dispatch_uid=uid, weak=False)
def model_pre_delete(sender: type[Model], instance: User | Group, **_):
"""Pre-delete handler"""
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
return
task_sync_direct.delay(
class_to_path(instance.__class__), instance.pk, Direction.remove.value
)
pre_delete.connect(model_pre_delete, User, dispatch_uid=uid, weak=False)
pre_delete.connect(model_pre_delete, Group, dispatch_uid=uid, weak=False)
def model_m2m_changed(
sender: type[Model], instance, action: str, pk_set: set, reverse: bool, **kwargs
):
"""Sync group membership"""
if action not in ["post_add", "post_remove"]:
return
if not provider_type.objects.filter(backchannel_application__isnull=False).exists():
return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups
if reverse:
task_sync_m2m.delay(str(instance.pk), action, list(pk_set))
else:
for group_pk in pk_set:
task_sync_m2m.delay(group_pk, action, [instance.pk])
m2m_changed.connect(model_m2m_changed, User.ak_groups.through, dispatch_uid=uid, weak=False)

View File

@ -0,0 +1,215 @@
from collections.abc import Callable
from celery.result import allow_join_result
from django.core.paginator import Paginator
from django.db.models import Model, QuerySet
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import BoundLogger, get_logger
from authentik.core.expression.exceptions import SkipObjectException
from authentik.core.models import Group, User
from authentik.events.logs import LogEvent
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask
from authentik.lib.sync.outgoing import PAGE_SIZE, PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.exceptions import StopSync, TransientSyncException
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
from authentik.lib.utils.reflection import class_to_path, path_to_class
class SyncTasks:
"""Container for all sync 'tasks' (this class doesn't actually contain celery
tasks due to celery's magic, however exposes a number of functions to be called from tasks)"""
logger: BoundLogger
def __init__(self, provider_model: type[OutgoingSyncProvider]) -> None:
super().__init__()
self._provider_model = provider_model
def sync_all(self, single_sync: Callable[[int], None]):
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
self.trigger_single_task(provider, single_sync)
def trigger_single_task(self, provider: OutgoingSyncProvider, sync_task: Callable[[int], None]):
"""Wrapper single sync task that correctly sets time limits based
on the amount of objects that will be synced"""
users_paginator = Paginator(provider.get_object_qs(User), PAGE_SIZE)
groups_paginator = Paginator(provider.get_object_qs(Group), PAGE_SIZE)
soft_time_limit = (users_paginator.num_pages + groups_paginator.num_pages) * PAGE_TIMEOUT
time_limit = soft_time_limit * 1.5
return sync_task.apply_async(
(provider.pk,), time_limit=int(time_limit), soft_time_limit=int(soft_time_limit)
)
def sync_single(
self,
task: SystemTask,
provider_pk: int,
sync_objects: Callable[[int, int], list[str]],
):
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk,
)
provider = self._provider_model.objects.filter(
pk=provider_pk, backchannel_application__isnull=False
).first()
if not provider:
return
lock = provider.sync_lock
if lock.locked():
self.logger.debug("Sync locked, skipping task", source=provider.name)
return
task.set_uid(slugify(provider.name))
messages = []
messages.append(_("Starting full provider sync"))
self.logger.debug("Starting provider sync")
users_paginator = Paginator(provider.get_object_qs(User), PAGE_SIZE)
groups_paginator = Paginator(provider.get_object_qs(Group), PAGE_SIZE)
with allow_join_result(), lock:
try:
for page in users_paginator.page_range:
messages.append(_("Syncing page %(page)d of users" % {"page": page}))
for msg in sync_objects.apply_async(
args=(class_to_path(User), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
).get():
messages.append(msg)
for page in groups_paginator.page_range:
messages.append(_("Syncing page %(page)d of groups" % {"page": page}))
for msg in sync_objects.apply_async(
args=(class_to_path(Group), page, provider_pk),
time_limit=PAGE_TIMEOUT,
soft_time_limit=PAGE_TIMEOUT,
).get():
messages.append(msg)
except TransientSyncException as exc:
self.logger.warning("transient sync exception", exc=exc)
raise task.retry(exc=exc) from exc
except StopSync as exc:
task.set_error(exc)
return
task.set_status(TaskStatus.SUCCESSFUL, *messages)
def sync_objects(self, object_type: str, page: int, provider_pk: int):
_object_type = path_to_class(object_type)
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
provider_pk=provider_pk,
object_type=object_type,
)
messages = []
provider = self._provider_model.objects.filter(pk=provider_pk).first()
if not provider:
return messages
try:
client = provider.client_for_model(_object_type)
except TransientSyncException:
return messages
paginator = Paginator(provider.get_object_qs(_object_type), PAGE_SIZE)
if client.can_discover:
self.logger.debug("starting discover")
client.discover()
self.logger.debug("starting sync for page", page=page)
for obj in paginator.page(page).object_list:
obj: Model
try:
client.write(obj)
except SkipObjectException:
continue
except TransientSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, user=obj)
messages.append(
LogEvent(
_(
(
"Failed to sync {object_type} {object_name} "
"due to transient error: {error}"
).format_map(
{
"object_type": obj._meta.verbose_name,
"object_name": str(obj),
"error": str(exc),
}
)
),
log_level="warning",
logger="",
)
)
except StopSync as exc:
self.logger.warning("Stopping sync", exc=exc)
messages.append(
LogEvent(
_(
"Stopping sync due to error: {error}".format_map(
{
"error": exc.detail(),
}
)
),
log_level="warning",
logger="",
)
)
break
return messages
def sync_signal_direct(self, model: str, pk: str | int, raw_op: str):
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
)
model_class: type[Model] = path_to_class(model)
instance = model_class.objects.filter(pk=pk).first()
if not instance:
return
operation = Direction(raw_op)
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
client = provider.client_for_model(instance.__class__)
# Check if the object is allowed within the provider's restrictions
queryset = provider.get_object_qs(instance.__class__)
if not queryset:
continue
# The queryset we get from the provider must include the instance we've got given
# otherwise ignore this provider
if not queryset.filter(pk=instance.pk).exists():
continue
try:
if operation == Direction.add:
client.write(instance)
if operation == Direction.remove:
client.delete(instance)
except (StopSync, TransientSyncException) as exc:
self.logger.warning(exc, provider_pk=provider.pk)
def sync_signal_m2m(self, group_pk: str, action: str, pk_set: list[int]):
self.logger = get_logger().bind(
provider_type=class_to_path(self._provider_model),
)
group = Group.objects.filter(pk=group_pk).first()
if not group:
return
for provider in self._provider_model.objects.filter(backchannel_application__isnull=False):
# Check if the object is allowed within the provider's restrictions
queryset: QuerySet = provider.get_object_qs(Group)
# The queryset we get from the provider must include the instance we've got given
# otherwise ignore this provider
if not queryset.filter(pk=group_pk).exists():
continue
client = provider.client_for_model(Group)
try:
operation = None
if action == "post_add":
operation = Direction.add
if action == "post_remove":
operation = Direction.remove
client.update_group(group, operation, pk_set)
except (StopSync, TransientSyncException) as exc:
self.logger.warning(exc, provider_pk=provider.pk)

View File

@ -24,7 +24,7 @@ def load_fixture(path: str, **kwargs) -> str:
fixture = _fixture.read()
try:
return fixture % kwargs
except TypeError:
except (TypeError, ValueError):
return fixture

View File

@ -96,16 +96,13 @@ class TestEvaluator(TestCase):
execution_logging=True,
expression="ak_message(request.http_request.path)\nreturn True",
)
tmpl = (
"""
tmpl = f"""
ak_message(request.http_request.path)
res = ak_call_policy('%s')
res = ak_call_policy('{expr.name}')
ak_message(request.http_request.path)
for msg in res.messages:
ak_message(msg)
"""
% expr.name
)
evaluator = PolicyEvaluator("test")
evaluator.set_policy_request(self.request)
res = evaluator.evaluate(tmpl)

View File

@ -326,7 +326,7 @@ class AuthorizationCode(SerializerModel, ExpiringModel, BaseGrantModel):
verbose_name_plural = _("Authorization Codes")
def __str__(self):
return f"Authorization code for {self.provider} for user {self.user}"
return f"Authorization code for {self.provider_id} for user {self.user_id}"
@property
def serializer(self) -> Serializer:
@ -356,7 +356,7 @@ class AccessToken(SerializerModel, ExpiringModel, BaseGrantModel):
verbose_name_plural = _("OAuth2 Access Tokens")
def __str__(self):
return f"Access Token for {self.provider} for user {self.user}"
return f"Access Token for {self.provider_id} for user {self.user_id}"
@property
def id_token(self) -> IDToken:
@ -399,7 +399,7 @@ class RefreshToken(SerializerModel, ExpiringModel, BaseGrantModel):
verbose_name_plural = _("OAuth2 Refresh Tokens")
def __str__(self):
return f"Refresh Token for {self.provider} for user {self.user}"
return f"Refresh Token for {self.provider_id} for user {self.user_id}"
@property
def id_token(self) -> IDToken:
@ -443,4 +443,4 @@ class DeviceToken(ExpiringModel):
verbose_name_plural = _("Device Tokens")
def __str__(self):
return f"Device Token for {self.provider}"
return f"Device Token for {self.provider_id}"

View File

@ -10,6 +10,7 @@ from jwt import PyJWKSet
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.crypto.builder import PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.generators import generate_id
from authentik.providers.oauth2.models import OAuth2Provider
@ -82,7 +83,7 @@ class TestJWKS(OAuthTestCase):
client_id="test",
authorization_flow=create_test_flow(),
redirect_uris="http://local.invalid",
signing_key=create_test_cert(use_ec_private_key=True),
signing_key=create_test_cert(PrivateKeyAlg.ECDSA),
)
app = Application.objects.create(name="test", slug="test", provider=provider)
response = self.client.get(

View File

@ -8,7 +8,7 @@ from django.views import View
from guardian.shortcuts import get_anonymous_user
from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import Application
from authentik.providers.oauth2.constants import (
ACR_AUTHENTIK_DEFAULT,

View File

@ -11,7 +11,7 @@ from django.views import View
from django.views.decorators.csrf import csrf_exempt
from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.events.models import Event, EventAction
from authentik.flows.challenge import PermissionDict
from authentik.providers.oauth2.constants import (

View File

@ -0,0 +1,44 @@
# Generated by Django 5.0.4 on 2024-05-01 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_saml", "0013_samlprovider_default_relay_state"),
]
operations = [
migrations.AlterField(
model_name="samlprovider",
name="digest_algorithm",
field=models.TextField(
choices=[
("http://www.w3.org/2000/09/xmldsig#sha1", "SHA1"),
("http://www.w3.org/2001/04/xmlenc#sha256", "SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#sha384", "SHA384"),
("http://www.w3.org/2001/04/xmlenc#sha512", "SHA512"),
],
default="http://www.w3.org/2001/04/xmlenc#sha256",
),
),
migrations.AlterField(
model_name="samlprovider",
name="signature_algorithm",
field=models.TextField(
choices=[
("http://www.w3.org/2000/09/xmldsig#rsa-sha1", "RSA-SHA1"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "RSA-SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "RSA-SHA384"),
("http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "RSA-SHA512"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1", "ECDSA-SHA1"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256", "ECDSA-SHA256"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384", "ECDSA-SHA384"),
("http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512", "ECDSA-SHA512"),
("http://www.w3.org/2000/09/xmldsig#dsa-sha1", "DSA-SHA1"),
],
default="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
),
),
]

View File

@ -11,6 +11,10 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import (
DSA_SHA1,
ECDSA_SHA1,
ECDSA_SHA256,
ECDSA_SHA384,
ECDSA_SHA512,
RSA_SHA1,
RSA_SHA256,
RSA_SHA384,
@ -92,8 +96,7 @@ class SAMLProvider(Provider):
),
)
digest_algorithm = models.CharField(
max_length=50,
digest_algorithm = models.TextField(
choices=(
(SHA1, _("SHA1")),
(SHA256, _("SHA256")),
@ -102,13 +105,16 @@ class SAMLProvider(Provider):
),
default=SHA256,
)
signature_algorithm = models.CharField(
max_length=50,
signature_algorithm = models.TextField(
choices=(
(RSA_SHA1, _("RSA-SHA1")),
(RSA_SHA256, _("RSA-SHA256")),
(RSA_SHA384, _("RSA-SHA384")),
(RSA_SHA512, _("RSA-SHA512")),
(ECDSA_SHA1, _("ECDSA-SHA1")),
(ECDSA_SHA256, _("ECDSA-SHA256")),
(ECDSA_SHA384, _("ECDSA-SHA384")),
(ECDSA_SHA512, _("ECDSA-SHA512")),
(DSA_SHA1, _("DSA-SHA1")),
),
default=RSA_SHA256,

View File

@ -9,7 +9,7 @@ from lxml import etree # nosec
from lxml.etree import Element, SubElement # nosec
from structlog.stdlib import get_logger
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.events.models import Event, EventAction
from authentik.events.signals import get_login_event
from authentik.lib.utils.time import timedelta_from_string

View File

@ -7,13 +7,14 @@ from lxml import etree # nosec
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_cert, create_test_flow
from authentik.crypto.builder import PrivateKeyAlg
from authentik.lib.generators import generate_id
from authentik.lib.tests.utils import load_fixture
from authentik.lib.xml import lxml_from_string
from authentik.providers.saml.models import SAMLBindings, SAMLPropertyMapping, SAMLProvider
from authentik.providers.saml.processors.metadata import MetadataProcessor
from authentik.providers.saml.processors.metadata_parser import ServiceProviderMetadataParser
from authentik.sources.saml.processors.constants import NS_MAP, NS_SAML_METADATA
from authentik.sources.saml.processors.constants import ECDSA_SHA256, NS_MAP, NS_SAML_METADATA
class TestServiceProviderMetadataParser(TestCase):
@ -107,12 +108,41 @@ class TestServiceProviderMetadataParser(TestCase):
load_fixture("fixtures/cert.xml").replace("/apps/user_saml", "")
)
def test_signature(self):
"""Test signature validation"""
def test_signature_rsa(self):
"""Test signature validation (RSA)"""
provider = SAMLProvider.objects.create(
name=generate_id(),
authorization_flow=self.flow,
signing_kp=create_test_cert(),
signing_kp=create_test_cert(PrivateKeyAlg.RSA),
)
Application.objects.create(
name=generate_id(),
slug=generate_id(),
provider=provider,
)
request = self.factory.get("/")
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
root = fromstring(metadata.encode())
xmlsec.tree.add_ids(root, ["ID"])
signature_nodes = root.xpath("/md:EntityDescriptor/ds:Signature", namespaces=NS_MAP)
signature_node = signature_nodes[0]
ctx = xmlsec.SignatureContext()
key = xmlsec.Key.from_memory(
provider.signing_kp.certificate_data,
xmlsec.constants.KeyDataFormatCertPem,
None,
)
ctx.key = key
ctx.verify(signature_node)
def test_signature_ecdsa(self):
"""Test signature validation (ECDSA)"""
provider = SAMLProvider.objects.create(
name=generate_id(),
authorization_flow=self.flow,
signing_kp=create_test_cert(PrivateKeyAlg.ECDSA),
signature_algorithm=ECDSA_SHA256,
)
Application.objects.create(
name=generate_id(),

View File

@ -1,19 +1,12 @@
"""SCIM Provider API Views"""
from django.utils.text import slugify
from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import BooleanField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_sync
class SCIMProviderSerializer(ProviderSerializer):
@ -40,14 +33,7 @@ class SCIMProviderSerializer(ProviderSerializer):
extra_kwargs = {}
class SCIMSyncStatusSerializer(PassiveSerializer):
"""SCIM Provider sync status"""
is_running = BooleanField(read_only=True)
tasks = SystemTaskSerializer(many=True, read_only=True)
class SCIMProviderViewSet(UsedByMixin, ModelViewSet):
class SCIMProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
"""SCIMProvider Viewset"""
queryset = SCIMProvider.objects.all()
@ -55,25 +41,4 @@ class SCIMProviderViewSet(UsedByMixin, ModelViewSet):
filterset_fields = ["name", "exclude_users_service_account", "url", "filter_group"]
search_fields = ["name", "url"]
ordering = ["name", "url"]
@extend_schema(
responses={
200: SCIMSyncStatusSerializer(),
404: OpenApiResponse(description="Task not found"),
}
)
@action(methods=["GET"], detail=True, pagination_class=None, filter_backends=[])
def sync_status(self, request: Request, pk: int) -> Response:
"""Get provider's sync status"""
provider: SCIMProvider = self.get_object()
tasks = list(
get_objects_for_user(request.user, "authentik_events.view_systemtask").filter(
name="scim_sync",
uid=slugify(provider.name),
)
)
status = {
"tasks": tasks,
"is_running": provider.sync_lock.locked(),
}
return Response(SCIMSyncStatusSerializer(status).data)
sync_single_task = scim_sync

View File

@ -1,4 +0,0 @@
"""SCIM constants"""
PAGE_SIZE = 100
PAGE_TIMEOUT = 60 * 60 * 0.5 # Half an hour

View File

@ -1,33 +1,37 @@
"""SCIM Client"""
from typing import Generic, TypeVar
from typing import TYPE_CHECKING
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from pydantic import ValidationError
from requests import RequestException, Session
from structlog.stdlib import get_logger
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, ObjectExistsSyncException
from authentik.lib.utils.http import get_http_session
from authentik.providers.scim.clients.exceptions import ResourceMissing, SCIMRequestException
from authentik.providers.scim.clients.exceptions import SCIMRequestException
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
from authentik.providers.scim.models import SCIMProvider
T = TypeVar("T")
SchemaType = TypeVar("SchemaType")
if TYPE_CHECKING:
from django.db.models import Model
from pydantic import BaseModel
class SCIMClient(Generic[T, SchemaType]):
class SCIMClient[TModel: "Model", TConnection: "Model", TSchema: "BaseModel"](
BaseOutgoingSyncClient[TModel, TConnection, TSchema, SCIMProvider]
):
"""SCIM Client"""
base_url: str
token: str
provider: SCIMProvider
_session: Session
_config: ServiceProviderConfiguration
def __init__(self, provider: SCIMProvider):
super().__init__(provider)
self._session = get_http_session()
self.provider = provider
# Remove trailing slashes as we assume the URL doesn't have any
@ -36,7 +40,6 @@ class SCIMClient(Generic[T, SchemaType]):
base_url = base_url[:-1]
self.base_url = base_url
self.token = provider.token
self.logger = get_logger().bind(provider=provider.name)
self._config = self.get_service_provider_config()
def _request(self, method: str, path: str, **kwargs) -> dict:
@ -57,7 +60,9 @@ class SCIMClient(Generic[T, SchemaType]):
self.logger.debug("scim request", path=path, method=method, **kwargs)
if response.status_code >= HttpResponseBadRequest.status_code:
if response.status_code == HttpResponseNotFound.status_code:
raise ResourceMissing(response)
raise NotFoundSyncException(response)
if response.status_code == HTTP_CONFLICT:
raise ObjectExistsSyncException(response)
self.logger.warning(
"Failed to send SCIM request", path=path, method=method, response=response.text
)
@ -76,15 +81,3 @@ class SCIMClient(Generic[T, SchemaType]):
except (ValidationError, SCIMRequestException) as exc:
self.logger.warning("failed to get ServiceProviderConfig", exc=exc)
return default_config
def write(self, obj: T):
"""Write object to SCIM"""
raise NotImplementedError()
def delete(self, obj: T):
"""Delete object from SCIM"""
raise NotImplementedError()
def to_scim(self, obj: T) -> SchemaType:
"""Convert object to scim"""
raise NotImplementedError()

View File

@ -3,28 +3,11 @@
from pydantic import ValidationError
from requests import Response
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.sync.outgoing.exceptions import TransientSyncException
from authentik.providers.scim.clients.schema import SCIMError
class StopSync(SentryIgnoredException):
"""Exception raised when a configuration error should stop the sync process"""
def __init__(self, exc: Exception, obj: object, mapping: object | None = None) -> None:
self.exc = exc
self.obj = obj
self.mapping = mapping
def detail(self) -> str:
"""Get human readable details of this error"""
msg = f"Error {str(self.exc)}, caused by {self.obj}"
if self.mapping:
msg += f" (mapping {self.mapping})"
return msg
class SCIMRequestException(SentryIgnoredException):
class SCIMRequestException(TransientSyncException):
"""Exception raised when an SCIM request fails"""
_response: Response | None
@ -39,13 +22,8 @@ class SCIMRequestException(SentryIgnoredException):
if not self._response:
return self._message
try:
error = SCIMError.parse_raw(self._response.text)
error = SCIMError.model_validate_json(self._response.text)
return error.detail
except ValidationError:
pass
return self._message
class ResourceMissing(SCIMRequestException):
"""Error raised when the provider raises a 404, meaning that we
should delete our internal ID and re-create the object"""

View File

@ -5,47 +5,36 @@ from pydantic import ValidationError
from pydanticscim.group import GroupMember
from pydanticscim.responses import PatchOp, PatchOperation
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import Group
from authentik.events.models import Event, EventAction
from authentik.lib.sync.outgoing.base import Direction
from authentik.lib.sync.outgoing.exceptions import (
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
)
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_values
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import (
ResourceMissing,
SCIMRequestException,
StopSync,
)
from authentik.providers.scim.clients.schema import Group as SCIMGroupSchema
from authentik.providers.scim.clients.schema import PatchRequest
from authentik.providers.scim.models import SCIMGroup, SCIMMapping, SCIMUser
class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
class SCIMGroupClient(SCIMClient[Group, SCIMGroup, SCIMGroupSchema]):
"""SCIM client for groups"""
def write(self, obj: Group):
"""Write a group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
return self._create(obj)
try:
return self._update(obj, scim_group)
except ResourceMissing:
scim_group.delete()
return self._create(obj)
connection_type = SCIMGroup
connection_type_query = "group"
def delete(self, obj: Group):
"""Delete group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Groups/{scim_group.id}")
scim_group.delete()
return response
def to_scim(self, obj: Group) -> SCIMGroupSchema:
def to_schema(self, obj: Group) -> SCIMGroupSchema:
"""Convert authentik user into SCIM"""
raw_scim_group = {
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:Group",),
@ -66,6 +55,8 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if value is None:
continue
always_merger.merge(raw_scim_group, value)
except SkipObjectException as exc:
raise exc from exc
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
@ -89,16 +80,26 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
for user in connections:
members.append(
GroupMember(
value=user.id,
value=user.scim_id,
)
)
if members:
scim_group.members = members
return scim_group
def _create(self, group: Group):
def delete(self, obj: Group):
"""Delete group"""
scim_group = SCIMGroup.objects.filter(provider=self.provider, group=obj).first()
if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Groups/{scim_group.scim_id}")
scim_group.delete()
return response
def create(self, group: Group):
"""Create group from scratch and create a connection object"""
scim_group = self.to_scim(group)
scim_group = self.to_schema(group)
response = self._request(
"POST",
"/Groups",
@ -107,25 +108,28 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
exclude_unset=True,
),
)
SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"])
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
def _update(self, group: Group, connection: SCIMGroup):
def update(self, group: Group, connection: SCIMGroup):
"""Update existing group"""
scim_group = self.to_scim(group)
scim_group.id = connection.id
scim_group = self.to_schema(group)
scim_group.id = connection.scim_id
try:
return self._request(
"PUT",
f"/Groups/{scim_group.id}",
f"/Groups/{connection.scim_id}",
json=scim_group.model_dump(
mode="json",
exclude_unset=True,
),
)
except ResourceMissing:
except NotFoundSyncException:
# Resource missing is handled by self.write, which will re-create the group
raise
except SCIMRequestException:
except (SCIMRequestException, ObjectExistsSyncException):
# Some providers don't support PUT on groups, so this is mainly a fix for the initial
# sync, send patch add requests for all the users the group currently has
users = list(group.users.order_by("id").values_list("id", flat=True))
@ -140,12 +144,12 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
),
)
def update_group(self, group: Group, action: PatchOp, users_set: set[int]):
def update_group(self, group: Group, action: Direction, users_set: set[int]):
"""Update a group, either using PUT to replace it or PATCH if supported"""
if self._config.patch.supported:
if action == PatchOp.add:
if action == Direction.add:
return self._patch_add_users(group, users_set)
if action == PatchOp.remove:
if action == Direction.remove:
return self._patch_remove_users(group, users_set)
try:
return self.write(group)
@ -153,9 +157,9 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if self._config.is_fallback:
# Assume that provider does not support PUT and also doesn't support
# ServiceProviderConfig, so try PATCH as a fallback
if action == PatchOp.add:
if action == Direction.add:
return self._patch_add_users(group, users_set)
if action == PatchOp.remove:
if action == Direction.remove:
return self._patch_remove_users(group, users_set)
raise exc
@ -185,13 +189,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
"scim_id", flat=True
)
)
if len(user_ids) < 1:
return
self._patch(
scim_group.id,
scim_group.scim_id,
PatchOperation(
op=PatchOp.add,
path="members",
@ -211,13 +215,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return
user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"id", flat=True
"scim_id", flat=True
)
)
if len(user_ids) < 1:
return
self._patch(
scim_group.id,
scim_group.scim_id,
PatchOperation(
op=PatchOp.remove,
path="members",

View File

@ -9,13 +9,14 @@ from pydanticscim.service_provider import (
)
from pydanticscim.user import User as BaseUser
SCIM_USER_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:User"
SCIM_GROUP_SCHEMA = "urn:ietf:params:scim:schemas:core:2.0:Group"
class User(BaseUser):
"""Modified User schema with added externalId field"""
schemas: list[str] = [
"urn:ietf:params:scim:schemas:core:2.0:User",
]
schemas: list[str] = [SCIM_USER_SCHEMA]
externalId: str | None = None
meta: dict | None = None
@ -23,9 +24,7 @@ class User(BaseUser):
class Group(BaseGroup):
"""Modified Group schema with added externalId field"""
schemas: list[str] = [
"urn:ietf:params:scim:schemas:core:2.0:Group",
]
schemas: list[str] = [SCIM_GROUP_SCHEMA]
externalId: str | None = None
meta: dict | None = None

View File

@ -3,42 +3,27 @@
from deepmerge import always_merger
from pydantic import ValidationError
from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import User
from authentik.events.models import Event, EventAction
from authentik.lib.sync.outgoing.exceptions import StopSync
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_values
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import ResourceMissing, StopSync
from authentik.providers.scim.clients.schema import User as SCIMUserSchema
from authentik.providers.scim.models import SCIMMapping, SCIMUser
class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
class SCIMUserClient(SCIMClient[User, SCIMUser, SCIMUserSchema]):
"""SCIM client for users"""
def write(self, obj: User):
"""Write a user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
return self._create(obj)
try:
return self._update(obj, scim_user)
except ResourceMissing:
scim_user.delete()
return self._create(obj)
connection_type = SCIMUser
connection_type_query = "user"
def delete(self, obj: User):
"""Delete user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Users/{scim_user.id}")
scim_user.delete()
return response
def to_scim(self, obj: User) -> SCIMUserSchema:
def to_schema(self, obj: User) -> SCIMUserSchema:
"""Convert authentik user into SCIM"""
raw_scim_user = {
"schemas": ("urn:ietf:params:scim:schemas:core:2.0:User",),
@ -56,6 +41,8 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
if value is None:
continue
always_merger.merge(raw_scim_user, value)
except SkipObjectException as exc:
raise exc from exc
except (PropertyMappingExpressionException, ValueError) as exc:
# Value error can be raised when assigning invalid data to an attribute
Event.new(
@ -74,9 +61,19 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
scim_user.externalId = str(obj.uid)
return scim_user
def _create(self, user: User):
def delete(self, obj: User):
"""Delete user"""
scim_user = SCIMUser.objects.filter(provider=self.provider, user=obj).first()
if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping")
return None
response = self._request("DELETE", f"/Users/{scim_user.scim_id}")
scim_user.delete()
return response
def create(self, user: User):
"""Create user from scratch and create a connection object"""
scim_user = self.to_scim(user)
scim_user = self.to_schema(user)
response = self._request(
"POST",
"/Users",
@ -85,15 +82,18 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
exclude_unset=True,
),
)
SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"])
scim_id = response.get("id")
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
def _update(self, user: User, connection: SCIMUser):
def update(self, user: User, connection: SCIMUser):
"""Update existing user"""
scim_user = self.to_scim(user)
scim_user.id = connection.id
scim_user = self.to_schema(user)
scim_user.id = connection.scim_id
self._request(
"PUT",
f"/Users/{connection.id}",
f"/Users/{connection.scim_id}",
json=scim_user.model_dump(
mode="json",
exclude_unset=True,

View File

@ -3,7 +3,7 @@
from structlog.stdlib import get_logger
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_sync, sync_tasks
from authentik.tenants.management import TenantCommand
LOGGER = get_logger()
@ -21,4 +21,4 @@ class Command(TenantCommand):
if not provider:
LOGGER.warning("Provider does not exist", name=provider_name)
continue
scim_sync.delay(provider.pk).get()
sync_tasks.trigger_single_task(provider, scim_sync).get()

View File

@ -0,0 +1,76 @@
# Generated by Django 5.0.4 on 2024-05-03 12:38
import uuid
from django.db import migrations, models
from django.apps.registry import Apps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from authentik.lib.migrations import progress_bar
def fix_scim_user_group_pk(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
SCIMUser = apps.get_model("authentik_providers_scim", "SCIMUser")
SCIMGroup = apps.get_model("authentik_providers_scim", "SCIMGroup")
db_alias = schema_editor.connection.alias
print("\nFixing primary key for SCIM users, this might take a couple of minutes...")
for user in progress_bar(SCIMUser.objects.using(db_alias).all()):
SCIMUser.objects.using(db_alias).filter(
pk=user.pk, user=user.user_id, provider=user.provider_id
).update(scim_id=user.pk, id=uuid.uuid4())
print("\nFixing primary key for SCIM groups, this might take a couple of minutes...")
for group in progress_bar(SCIMGroup.objects.using(db_alias).all()):
SCIMGroup.objects.using(db_alias).filter(
pk=group.pk, group=group.group_id, provider=group.provider_id
).update(scim_id=group.pk, id=uuid.uuid4())
class Migration(migrations.Migration):
dependencies = [
(
"authentik_providers_scim",
"0001_squashed_0006_rename_parent_group_scimprovider_filter_group",
),
]
operations = [
migrations.AddField(
model_name="scimgroup",
name="scim_id",
field=models.TextField(default="temp"),
preserve_default=False,
),
migrations.AddField(
model_name="scimuser",
name="scim_id",
field=models.TextField(default="temp"),
preserve_default=False,
),
migrations.RunPython(fix_scim_user_group_pk),
migrations.AlterField(
model_name="scimgroup",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
migrations.AlterField(
model_name="scimuser",
name="id",
field=models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
migrations.AlterField(model_name="scimuser", name="scim_id", field=models.TextField()),
migrations.AlterField(model_name="scimgroup", name="scim_id", field=models.TextField()),
migrations.AlterUniqueTogether(
name="scimgroup",
unique_together={("scim_id", "group", "provider")},
),
migrations.AlterUniqueTogether(
name="scimuser",
unique_together={("scim_id", "user", "provider")},
),
]

View File

@ -1,17 +1,19 @@
"""SCIM Provider models"""
from django.core.cache import cache
from typing import Any, Self
from uuid import uuid4
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from redis.lock import Lock
from rest_framework.serializers import Serializer
from authentik.core.models import BackchannelProvider, Group, PropertyMapping, User, UserTypes
from authentik.providers.scim.clients import PAGE_TIMEOUT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
class SCIMProvider(BackchannelProvider):
class SCIMProvider(OutgoingSyncProvider, BackchannelProvider):
"""SCIM 2.0 provider to create users and groups in external applications"""
exclude_users_service_account = models.BooleanField(default=False)
@ -30,30 +32,35 @@ class SCIMProvider(BackchannelProvider):
help_text=_("Property mappings used for group creation/updating."),
)
@property
def sync_lock(self) -> Lock:
"""Redis lock for syncing SCIM to prevent multiple parallel syncs happening"""
return Lock(
cache.client.get_client(),
name=f"goauthentik.io/providers/scim/sync-{str(self.pk)}",
timeout=(60 * 60 * PAGE_TIMEOUT) * 3,
)
def client_for_model(
self, model: type[User | Group]
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User):
from authentik.providers.scim.clients.users import SCIMUserClient
def get_user_qs(self) -> QuerySet[User]:
"""Get queryset of all users with consistent ordering
according to the provider's settings"""
base = User.objects.all().exclude_anonymous()
if self.exclude_users_service_account:
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
)
if self.filter_group:
base = base.filter(ak_groups__in=[self.filter_group])
return base.order_by("pk")
return SCIMUserClient(self)
if issubclass(model, Group):
from authentik.providers.scim.clients.groups import SCIMGroupClient
def get_group_qs(self) -> QuerySet[Group]:
"""Get queryset of all groups with consistent ordering"""
return Group.objects.all().order_by("pk")
return SCIMGroupClient(self)
raise ValueError(f"Invalid model {model}")
def get_object_qs(self, type: type[User | Group]) -> QuerySet[User | Group]:
if type == User:
# Get queryset of all users with consistent ordering
# according to the provider's settings
base = User.objects.all().exclude_anonymous()
if self.exclude_users_service_account:
base = base.exclude(type=UserTypes.SERVICE_ACCOUNT).exclude(
type=UserTypes.INTERNAL_SERVICE_ACCOUNT
)
if self.filter_group:
base = base.filter(ak_groups__in=[self.filter_group])
return base.order_by("pk")
if type == Group:
# Get queryset of all groups with consistent ordering
return Group.objects.all().order_by("pk")
raise ValueError(f"Invalid type {type}")
@property
def component(self) -> str:
@ -82,7 +89,7 @@ class SCIMMapping(PropertyMapping):
@property
def serializer(self) -> type[Serializer]:
from authentik.providers.scim.api.property_mapping import SCIMMappingSerializer
from authentik.providers.scim.api.property_mappings import SCIMMappingSerializer
return SCIMMappingSerializer
@ -97,26 +104,28 @@ class SCIMMapping(PropertyMapping):
class SCIMUser(models.Model):
"""Mapping of a user and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
scim_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "user", "provider"),)
unique_together = (("scim_id", "user", "provider"),)
def __str__(self) -> str:
return f"SCIM User {self.user.username} to {self.provider.name}"
return f"SCIM User {self.user_id} to {self.provider_id}"
class SCIMGroup(models.Model):
"""Mapping of a group and provider to a SCIM user ID"""
id = models.TextField(primary_key=True)
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
scim_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta:
unique_together = (("id", "group", "provider"),)
unique_together = (("scim_id", "group", "provider"),)
def __str__(self) -> str:
return f"SCIM Group {self.group.name} to {self.provider.name}"
return f"SCIM Group {self.group_id} to {self.provider_id}"

View File

@ -7,7 +7,7 @@ from authentik.lib.utils.time import fqdn_rand
CELERY_BEAT_SCHEDULE = {
"providers_scim_sync": {
"task": "authentik.providers.scim.tasks.scim_sync_all",
"schedule": crontab(minute=fqdn_rand("scim_sync_all"), hour="*"),
"schedule": crontab(minute=fqdn_rand("scim_sync_all"), hour="*/4"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -1,56 +1,12 @@
"""SCIM provider signals"""
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete
from django.dispatch import receiver
from pydanticscim.responses import PatchOp
from structlog.stdlib import get_logger
from authentik.core.models import Group, User
from authentik.lib.utils.reflection import class_to_path
from authentik.lib.sync.outgoing.signals import register_signals
from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_sync
from authentik.providers.scim.tasks import scim_sync, scim_sync_direct, scim_sync_m2m
LOGGER = get_logger()
@receiver(post_save, sender=SCIMProvider)
def post_save_provider(sender: type[Model], instance, created: bool, **_):
"""Trigger sync when SCIM provider is saved"""
scim_sync.delay(instance.pk)
@receiver(post_save, sender=User)
@receiver(post_save, sender=Group)
def post_save_scim(sender: type[Model], instance: User | Group, created: bool, **_):
"""Post save handler"""
if not SCIMProvider.objects.filter(backchannel_application__isnull=False).exists():
return
scim_signal_direct.delay(class_to_path(instance.__class__), instance.pk, PatchOp.add.value)
@receiver(pre_delete, sender=User)
@receiver(pre_delete, sender=Group)
def pre_delete_scim(sender: type[Model], instance: User | Group, **_):
"""Pre-delete handler"""
if not SCIMProvider.objects.filter(backchannel_application__isnull=False).exists():
return
scim_signal_direct.delay(class_to_path(instance.__class__), instance.pk, PatchOp.remove.value)
@receiver(m2m_changed, sender=User.ak_groups.through)
def m2m_changed_scim(
sender: type[Model], instance, action: str, pk_set: set, reverse: bool, **kwargs
):
"""Sync group membership"""
if action not in ["post_add", "post_remove"]:
return
if not SCIMProvider.objects.filter(backchannel_application__isnull=False).exists():
return
# reverse: instance is a Group, pk_set is a list of user pks
# non-reverse: instance is a User, pk_set is a list of groups
if reverse:
scim_signal_m2m.delay(str(instance.pk), action, list(pk_set))
else:
for group_pk in pk_set:
scim_signal_m2m.delay(group_pk, action, [instance.pk])
register_signals(
SCIMProvider,
task_sync_single=scim_sync,
task_sync_direct=scim_sync_direct,
task_sync_m2m=scim_sync_m2m,
)

View File

@ -1,226 +1,34 @@
"""SCIM Provider tasks"""
from typing import Any
from celery.result import allow_join_result
from django.core.paginator import Paginator
from django.db.models import Model, QuerySet
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from pydanticscim.responses import PatchOp
from structlog.stdlib import get_logger
from authentik.core.models import Group, User
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask
from authentik.lib.utils.reflection import path_to_class
from authentik.providers.scim.clients import PAGE_SIZE, PAGE_TIMEOUT
from authentik.providers.scim.clients.base import SCIMClient
from authentik.providers.scim.clients.exceptions import SCIMRequestException, StopSync
from authentik.providers.scim.clients.group import SCIMGroupClient
from authentik.providers.scim.clients.user import SCIMUserClient
from authentik.lib.sync.outgoing.tasks import SyncTasks
from authentik.providers.scim.models import SCIMProvider
from authentik.root.celery import CELERY_APP
LOGGER = get_logger(__name__)
sync_tasks = SyncTasks(SCIMProvider)
def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
"""Get SCIM client for model"""
if isinstance(model, User):
return SCIMUserClient(provider)
if isinstance(model, Group):
return SCIMGroupClient(provider)
raise ValueError(f"Invalid model {model}")
@CELERY_APP.task()
def scim_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@CELERY_APP.task(base=SystemTask, bind=True)
def scim_sync(self, provider_pk: int, *args, **kwargs):
"""Run full sync for SCIM provider"""
return sync_tasks.sync_single(self, provider_pk, scim_sync_objects)
@CELERY_APP.task()
def scim_sync_all():
"""Run sync for all providers"""
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
scim_sync.delay(provider.pk)
@CELERY_APP.task(bind=True, base=SystemTask)
def scim_sync(self: SystemTask, provider_pk: int) -> None:
"""Run SCIM full sync for provider"""
provider: SCIMProvider = SCIMProvider.objects.filter(
pk=provider_pk, backchannel_application__isnull=False
).first()
if not provider:
return
lock = provider.sync_lock
if lock.locked():
LOGGER.debug("SCIM sync locked, skipping task", source=provider.name)
return
self.set_uid(slugify(provider.name))
messages = []
messages.append(_("Starting full SCIM sync"))
LOGGER.debug("Starting SCIM sync")
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
self.soft_time_limit = self.time_limit = (
users_paginator.count + groups_paginator.count
) * PAGE_TIMEOUT
with allow_join_result():
try:
for page in users_paginator.page_range:
messages.append(_("Syncing page %(page)d of users" % {"page": page}))
for msg in scim_sync_users.delay(page, provider_pk).get():
messages.append(msg)
for page in groups_paginator.page_range:
messages.append(_("Syncing page %(page)d of groups" % {"page": page}))
for msg in scim_sync_group.delay(page, provider_pk).get():
messages.append(msg)
except StopSync as exc:
self.set_error(exc)
return
self.set_status(TaskStatus.SUCCESSFUL, *messages)
@CELERY_APP.task(
soft_time_limit=PAGE_TIMEOUT,
task_time_limit=PAGE_TIMEOUT,
)
def scim_sync_users(page: int, provider_pk: int):
"""Sync single or multiple users to SCIM"""
messages = []
provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first()
if not provider:
return messages
try:
client = SCIMUserClient(provider)
except SCIMRequestException:
return messages
paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
LOGGER.debug("starting user sync for page", page=page)
for user in paginator.page(page).object_list:
try:
client.write(user)
except SCIMRequestException as exc:
LOGGER.warning("failed to sync user", exc=exc, user=user)
messages.append(
_(
"Failed to sync user {user_name} due to remote error: {error}".format_map(
{
"user_name": user.username,
"error": exc.detail(),
}
)
)
)
except StopSync as exc:
LOGGER.warning("Stopping sync", exc=exc)
messages.append(
_(
"Stopping sync due to error: {error}".format_map(
{
"error": exc.detail(),
}
)
)
)
break
return messages
return sync_tasks.sync_all(scim_sync)
@CELERY_APP.task()
def scim_sync_group(page: int, provider_pk: int):
"""Sync single or multiple groups to SCIM"""
messages = []
provider: SCIMProvider = SCIMProvider.objects.filter(pk=provider_pk).first()
if not provider:
return messages
try:
client = SCIMGroupClient(provider)
except SCIMRequestException:
return messages
paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
LOGGER.debug("starting group sync for page", page=page)
for group in paginator.page(page).object_list:
try:
client.write(group)
except SCIMRequestException as exc:
LOGGER.warning("failed to sync group", exc=exc, group=group)
messages.append(
_(
"Failed to sync group {group_name} due to remote error: {error}".format_map(
{
"group_name": group.name,
"error": exc.detail(),
}
)
)
)
except StopSync as exc:
LOGGER.warning("Stopping sync", exc=exc)
messages.append(
_(
"Stopping sync due to error: {error}".format_map(
{
"error": exc.detail(),
}
)
)
)
break
return messages
def scim_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@CELERY_APP.task()
def scim_signal_direct(model: str, pk: Any, raw_op: str):
"""Handler for post_save and pre_delete signal"""
model_class: type[Model] = path_to_class(model)
instance = model_class.objects.filter(pk=pk).first()
if not instance:
return
operation = PatchOp(raw_op)
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
client = client_for_model(provider, instance)
# Check if the object is allowed within the provider's restrictions
queryset: QuerySet | None = None
if isinstance(instance, User):
queryset = provider.get_user_qs()
if isinstance(instance, Group):
queryset = provider.get_group_qs()
if not queryset:
continue
# The queryset we get from the provider must include the instance we've got given
# otherwise ignore this provider
if not queryset.filter(pk=instance.pk).exists():
continue
try:
if operation == PatchOp.add:
client.write(instance)
if operation == PatchOp.remove:
client.delete(instance)
except (StopSync, SCIMRequestException) as exc:
LOGGER.warning(exc)
@CELERY_APP.task()
def scim_signal_m2m(group_pk: str, action: str, pk_set: list[int]):
"""Update m2m (group membership)"""
group = Group.objects.filter(pk=group_pk).first()
if not group:
return
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
# Check if the object is allowed within the provider's restrictions
queryset: QuerySet = provider.get_group_qs()
# The queryset we get from the provider must include the instance we've got given
# otherwise ignore this provider
if not queryset.filter(pk=group_pk).exists():
continue
client = SCIMGroupClient(provider)
try:
operation = None
if action == "post_add":
operation = PatchOp.add
if action == "post_remove":
operation = PatchOp.remove
client.update_group(group, operation, pk_set)
except (StopSync, SCIMRequestException) as exc:
LOGGER.warning(exc)
def scim_sync_m2m(*args, **kwargs):
return sync_tasks.sync_signal_m2m(*args, **kwargs)

View File

@ -8,7 +8,7 @@ from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.clients.schema import ServiceProviderConfiguration
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_sync, sync_tasks
from authentik.tenants.models import Tenant
@ -79,7 +79,7 @@ class SCIMMembershipTests(TestCase):
)
self.configure()
scim_sync.delay(self.provider.pk).get()
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
self.assertEqual(mocker.call_count, 6)
self.assertEqual(mocker.request_history[0].method, "GET")
@ -169,7 +169,7 @@ class SCIMMembershipTests(TestCase):
)
self.configure()
scim_sync.delay(self.provider.pk).get()
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
self.assertEqual(mocker.call_count, 6)
self.assertEqual(mocker.request_history[0].method, "GET")

View File

@ -10,7 +10,7 @@ from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.lib.generators import generate_id
from authentik.providers.scim.models import SCIMMapping, SCIMProvider
from authentik.providers.scim.tasks import scim_sync
from authentik.providers.scim.tasks import scim_sync, sync_tasks
from authentik.tenants.models import Tenant
@ -88,6 +88,72 @@ class SCIMUserTests(TestCase):
},
)
@Mocker()
def test_user_create_different_provider_same_id(self, mock: Mocker):
"""Test user creation with multiple providers that happen
to return the same object ID"""
# Create duplicate provider
provider: SCIMProvider = SCIMProvider.objects.create(
name=generate_id(),
url="https://localhost",
token=generate_id(),
exclude_users_service_account=True,
)
app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
app.backchannel_providers.add(provider)
provider.property_mappings.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/user")
)
provider.property_mappings_group.add(
SCIMMapping.objects.get(managed="goauthentik.io/providers/scim/group")
)
scim_id = generate_id()
mock.get(
"https://localhost/ServiceProviderConfig",
json={},
)
mock.post(
"https://localhost/Users",
json={
"id": scim_id,
},
)
uid = generate_id()
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
self.assertEqual(mock.call_count, 4)
self.assertEqual(mock.request_history[0].method, "GET")
self.assertEqual(mock.request_history[1].method, "POST")
self.assertJSONEqual(
mock.request_history[1].body,
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"active": True,
"emails": [
{
"primary": True,
"type": "other",
"value": f"{uid}@goauthentik.io",
}
],
"externalId": user.uid,
"name": {
"familyName": uid,
"formatted": f"{uid} {uid}",
"givenName": uid,
},
"displayName": f"{uid} {uid}",
"userName": uid,
},
)
@Mocker()
def test_user_create_update(self, mock: Mocker):
"""Test user creation and update"""
@ -236,7 +302,7 @@ class SCIMUserTests(TestCase):
email=f"{uid}@goauthentik.io",
)
scim_sync.delay(self.provider.pk).get()
sync_tasks.trigger_single_task(self.provider, scim_sync).get()
self.assertEqual(mock.call_count, 5)
self.assertEqual(mock.request_history[0].method, "GET")

View File

@ -1,6 +1,6 @@
"""API URLs"""
from authentik.providers.scim.api.property_mapping import SCIMMappingViewSet
from authentik.providers.scim.api.property_mappings import SCIMMappingViewSet
from authentik.providers.scim.api.providers import SCIMProviderViewSet
api_urlpatterns = [

View File

@ -155,6 +155,9 @@ SPECTACULAR_SETTINGS = {
"LDAPAPIAccessMode": "authentik.providers.ldap.models.APIAccessMode",
"UserVerificationEnum": "authentik.stages.authenticator_webauthn.models.UserVerification",
"UserTypeEnum": "authentik.core.models.UserTypes",
"GoogleWorkspaceDeleteAction": (
"authentik.enterprise.providers.google_workspace.models.GoogleWorkspaceDeleteAction"
),
},
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
"ENUM_GENERATE_CHOICE_DESCRIPTION": False,
@ -376,7 +379,13 @@ CELERY = {
"task_default_queue": "authentik",
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
"result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
"broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
"broker_transport_options": CONFIG.get_dict_from_b64_json(
"broker.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"result_backend_transport_options": CONFIG.get_dict_from_b64_json(
"result_backend.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"redis_retry_on_timeout": True,
}
# Sentry integration

Some files were not shown because too many files have changed in this diff Show More