Compare commits

..

205 Commits

Author SHA1 Message Date
a5379c35aa add to user
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 18:00:00 +02:00
e4c11a5284 manager for deleted objects
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:59:06 +02:00
a4853a1e09 migrate outpost to soft-delete
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:59:06 +02:00
b65b72d910 core: exclude anonymous user by default
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:59:06 +02:00
cd7be6a1a4 initial soft delete
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:58:03 +02:00
e5cb8ef541 unrelated reorganization
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 17:58:01 +02:00
919b56c466 web/admin: fix entra provider mapping form (#9767)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-18 14:57:16 +02:00
db7bc8b7ad Change custom pipeline since netbox 4.0.0 (#9743)
Due to a Django 5.0 regression, group.user_set does not exist anymore.  
https://github.com/netbox-community/netbox/discussions/16098
https://github.com/netbox-community/netbox/discussions/16000

Signed-off-by: Manuel Rais <manuel.rais@nousvoir.com>
2024-05-17 17:04:41 -05:00
5768cb5858 web: bump @sentry/browser from 7.114.0 to 8.2.1 in /web in the sentry group across 1 directory (#9757)
* web: bump @sentry/browser in /web in the sentry group across 1 directory

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


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

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

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

* fix sentry beforeSend

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-05-17 12:43:33 +02:00
5b77bc33c7 core, web: update translations (#9714)
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-17 12:32:05 +02:00
93650e925a core: bump sentry-sdk from 2.1.1 to 2.2.0 (#9753)
Bumps [sentry-sdk](https://github.com/getsentry/sentry-python) from 2.1.1 to 2.2.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/2.1.1...2.2.0)

---
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-17 12:19:49 +02:00
83823068fe core: bump selenium from 4.20.0 to 4.21.0 (#9754)
Bumps [selenium](https://github.com/SeleniumHQ/Selenium) from 4.20.0 to 4.21.0.
- [Release notes](https://github.com/SeleniumHQ/Selenium/releases)
- [Commits](https://github.com/SeleniumHQ/Selenium/compare/selenium-4.20.0...selenium-4.21.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-05-17 12:18:22 +02:00
d922f41438 core: bump msgraph-sdk from 1.2.0 to 1.4.0 (#9755)
Bumps [msgraph-sdk](https://github.com/microsoftgraph/msgraph-sdk-python) from 1.2.0 to 1.4.0.
- [Release notes](https://github.com/microsoftgraph/msgraph-sdk-python/releases)
- [Changelog](https://github.com/microsoftgraph/msgraph-sdk-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/microsoftgraph/msgraph-sdk-python/compare/v1.2.0...v1.4.0)

---
updated-dependencies:
- dependency-name: msgraph-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-17 12:18:11 +02:00
ef3d545d7a core: bump github.com/sethvargo/go-envconfig from 1.0.1 to 1.0.2 (#9756)
Bumps [github.com/sethvargo/go-envconfig](https://github.com/sethvargo/go-envconfig) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/sethvargo/go-envconfig/releases)
- [Commits](https://github.com/sethvargo/go-envconfig/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: github.com/sethvargo/go-envconfig
  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-17 12:17:55 +02:00
e9efbb2161 web: bump chromedriver from 124.0.3 to 125.0.0 in /tests/wdio (#9758)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 124.0.3 to 125.0.0.
- [Commits](https://github.com/giggio/node-chromedriver/compare/124.0.3...125.0.0)

---
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-05-17 12:17:37 +02:00
a3634ab21d website/docs: new PR for the Entra provider docs (ignore old one) (#9741)
* new PR

* ken edits

* tweaks

---------

Co-authored-by: Tana M Berry <tana@goauthentik.com>
2024-05-16 17:00:03 -05:00
f28209548b root: include task_id in events and logs (#9749)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-16 13:33:00 +02:00
9589e04072 web: bump the esbuild group in /web with 2 updates (#9745)
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.2 to 0.21.3
- [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.2...v0.21.3)

Updates `@esbuild/linux-arm64` from 0.21.2 to 0.21.3
- [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.2...v0.21.3)

---
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-16 12:23:20 +02:00
6490703ba3 web: bump esbuild from 0.21.2 to 0.21.3 in /web (#9746)
Bumps [esbuild](https://github.com/evanw/esbuild) from 0.21.2 to 0.21.3.
- [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.2...v0.21.3)

---
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-16 12:23:06 +02:00
ca2fce05f5 web: bump the storybook group across 1 directory with 7 updates (#9747)
Bumps the storybook group with 6 updates in the /web directory:

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



Updates `@storybook/addon-essentials` from 8.0.10 to 8.1.1
- [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.1.1/code/addons/essentials)

Updates `@storybook/addon-links` from 8.0.10 to 8.1.1
- [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.1.1/code/addons/links)

Updates `@storybook/blocks` from 8.0.10 to 8.1.1
- [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.1.1/code/ui/blocks)

Updates `@storybook/manager-api` from 8.0.10 to 8.1.1
- [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.1.1/code/lib/manager-api)

Updates `@storybook/web-components` from 8.0.10 to 8.1.1
- [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.1.1/code/renderers/web-components)

Updates `@storybook/web-components-vite` from 8.0.10 to 8.1.1
- [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.1.1/code/frameworks/web-components-vite)

Updates `storybook` from 8.0.10 to 8.1.1
- [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.1.1/code/lib/cli)

---
updated-dependencies:
- dependency-name: "@storybook/addon-essentials"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/addon-links"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/blocks"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/manager-api"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/web-components"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: "@storybook/web-components-vite"
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: storybook
- dependency-name: storybook
  dependency-type: direct:development
  update-type: version-update:semver-minor
  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-16 12:22:54 +02:00
9dc813d9ab website/docs: update traefik to latest version in proxy provider (#9707) 2024-05-13 20:00:37 +00:00
833c66a9dd sources/saml: fix FlowPlanner error due to pickle (#9708)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-13 20:33:34 +02:00
5d54f696d4 website/docs: add docs about Google Workspace (#9669)
* stub files

* tweaks

* add to sidebar

* tweaks

* steps to set up gws

* first drafts

* link

* unsaved

* formatting

* typos

* add Ent badge

* backchannel and otehr edits

* tweaks

* tweaks

* rewrite stuff

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

* em one word

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Jens Langhammer <jens@goauthentik.io>
2024-05-13 20:33:06 +02:00
0746652995 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9702)
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-13 19:36:53 +02:00
a1a55c644a translate: Updates for file web/xliff/en.xlf in zh_CN (#9703)
* Translate web/xliff/en.xlf in zh_CN

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

* 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-13 19:36:38 +02:00
fce57d258e translate: Updates for file web/xliff/en.xlf in zh-Hans (#9705)
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-13 19:36:14 +02:00
17e30b7adc translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9706)
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-13 19:35:58 +02:00
c5b3f8a578 web: bump esbuild from 0.21.1 to 0.21.2 in /web (#9696) 2024-05-13 13:41:46 +02:00
3a3619fa18 web: bump chromedriver from 124.0.2 to 124.0.3 in /tests/wdio (#9692)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 124.0.2 to 124.0.3.
- [Commits](https://github.com/giggio/node-chromedriver/compare/124.0.2...124.0.3)

---
updated-dependencies:
- dependency-name: chromedriver
  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-13 11:41:12 +02:00
21b933efff website: bump @types/react from 18.3.1 to 18.3.2 in /website (#9691)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 18.3.1 to 18.3.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- 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-05-13 11:40:38 +02:00
97fc2cba69 core, web: update translations (#9672)
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-13 11:40:14 +02:00
7ef627d476 core: bump psycopg from 3.1.18 to 3.1.19 (#9698)
Bumps [psycopg](https://github.com/psycopg/psycopg) from 3.1.18 to 3.1.19.
- [Changelog](https://github.com/psycopg/psycopg/blob/master/docs/news.rst)
- [Commits](https://github.com/psycopg/psycopg/compare/3.1.18...3.1.19)

---
updated-dependencies:
- dependency-name: psycopg
  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-13 11:36:47 +02:00
d16c603499 core: bump google-api-python-client from 2.128.0 to 2.129.0 (#9694)
Bumps [google-api-python-client](https://github.com/googleapis/google-api-python-client) from 2.128.0 to 2.129.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.128.0...v2.129.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-13 11:35:37 +02:00
99a69bb52f web: bump the esbuild group in /web with 2 updates (#9695)
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.1 to 0.21.2
- [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.1...v0.21.2)

Updates `@esbuild/linux-arm64` from 0.21.1 to 0.21.2
- [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.1...v0.21.2)

---
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-13 11:35:10 +02:00
ac8192d660 web: bump glob from 10.3.14 to 10.3.15 in /web (#9697)
Bumps [glob](https://github.com/isaacs/node-glob) from 10.3.14 to 10.3.15.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.3.14...v10.3.15)

---
updated-dependencies:
- dependency-name: glob
  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-13 11:34:50 +02:00
cdf3449230 core: bump freezegun from 1.5.0 to 1.5.1 (#9693)
Bumps [freezegun](https://github.com/spulec/freezegun) from 1.5.0 to 1.5.1.
- [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.5.0...1.5.1)

---
updated-dependencies:
- dependency-name: freezegun
  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-13 11:33:24 +02:00
ef2a40ed7d web: fix value handling inside controlled components (#9648)
* 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: 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.

* Eslint had opinions.

* Added comments to explain something.
2024-05-10 09:50:07 -07:00
09cacbd76b website/docs: add hardening advice and link directly to Cure53 results (#9670)
docs: add hardening advice and link directly to Cure53 results

Signed-off-by: Fletcher Heisler <fheisler@users.noreply.github.com>
2024-05-10 09:07:19 -04:00
cb33f0d1e2 core: bump goauthentik.io/api/v3 from 3.2024042.2 to 3.2024042.4 (#9674)
Bumps [goauthentik.io/api/v3](https://github.com/goauthentik/client-go) from 3.2024042.2 to 3.2024042.4.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v3.2024042.2...v3.2024042.4)

---
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-10 12:13:36 +02:00
90af4b29a6 core: bump ruff from 0.4.3 to 0.4.4 (#9677)
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.4.3 to 0.4.4.
- [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.3...v0.4.4)

---
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-10 12:13:26 +02:00
6b9158591e core: bump github.com/prometheus/client_golang from 1.19.0 to 1.19.1 (#9675)
Bumps [github.com/prometheus/client_golang](https://github.com/prometheus/client_golang) from 1.19.0 to 1.19.1.
- [Release notes](https://github.com/prometheus/client_golang/releases)
- [Changelog](https://github.com/prometheus/client_golang/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prometheus/client_golang/compare/v1.19.0...v1.19.1)

---
updated-dependencies:
- dependency-name: github.com/prometheus/client_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-10 12:13:16 +02:00
9c15cda191 web: bump glob from 10.3.12 to 10.3.14 in /web (#9676)
Bumps [glob](https://github.com/isaacs/node-glob) from 10.3.12 to 10.3.14.
- [Changelog](https://github.com/isaacs/node-glob/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/node-glob/compare/v10.3.12...v10.3.14)

---
updated-dependencies:
- dependency-name: glob
  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-10 12:13:05 +02:00
046b8d5cbf lib/providers/sync: multiple minor fixes (#9667)
* don't propagate from signal

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

* improve retry

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-09 22:02:13 +02:00
8b74b83983 core: fix source flow_manager not always appending save stage (#9659)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-09 19:04:32 +02:00
8de038b387 web: bump API Client version (#9660)
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-09 16:33:56 +00:00
2edc651582 web/admin: only show non-backchannel providers in application provider select (#9658)
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-09 18:10:07 +02:00
85594a119c website/docs: add new doc about extra steps for hardening authentik (#9649)
* add to sidebar

* tweaks

* tweaks

* add derek edit

* ken edit

* Update website/docs/security/security-hardening.md

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

* tweaks

---------

Signed-off-by: Tana M Berry <tanamarieberry@yahoo.com>
Co-authored-by: Tana M Berry <tana@goauthentik.com>
Co-authored-by: Jens L. <jens@goauthentik.io>
2024-05-09 10:02:51 -05:00
1a97ccea03 web: bump API Client version (#9656) 2024-05-09 15:46:01 +02:00
99ad492951 enterprise/providers/microsoft_entra: initial account sync to microsoft entra (#9632)
* initial

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

* add entra mappings

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

* fix some stuff

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

* make API endpoints more consistent

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

* implement more things

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

* add user tests

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

* fix most group tests + fix bugs

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

* more group tests, fix bugs

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

* fix missing __init__

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

* add ui for provisioned users

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

* fix a bunch of bugs

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

* add `creating` to property mapping env

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

* always sync group members

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

* fix stuff

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

* fix group membership

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

* fix some types

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

* fix tests

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

* add group member add test

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

* create sync status component to dedupe

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

* fix discovery tests

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

* get rid of more code and fix more issues

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

* add error handling for auth and transient

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

* make sure autoretry is on

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

* format web

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

* wait for task in signal

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

* fix tests

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

* add squashed google migration

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

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-05-09 15:41:23 +02:00
ff4ec6f9b4 web: bump chromedriver from 124.0.1 to 124.0.2 in /tests/wdio (#9652)
Bumps [chromedriver](https://github.com/giggio/node-chromedriver) from 124.0.1 to 124.0.2.
- [Commits](https://github.com/giggio/node-chromedriver/compare/124.0.1...124.0.2)

---
updated-dependencies:
- dependency-name: chromedriver
  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-09 13:10:02 +02:00
0c49de67b8 web: bump @sentry/browser from 7.113.0 to 7.114.0 in /web in the sentry group (#9653)
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.113.0 to 7.114.0
- [Release notes](https://github.com/getsentry/sentry-javascript/releases)
- [Changelog](https://github.com/getsentry/sentry-javascript/blob/7.114.0/CHANGELOG.md)
- [Commits](https://github.com/getsentry/sentry-javascript/compare/7.113.0...7.114.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-09 13:09:52 +02:00
0d73528ec7 core, web: update translations (#9650) 2024-05-09 02:48:01 +02:00
80ca4e5722 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#9644)
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-08 23:28:02 +02:00
1ebe200a46 translate: Updates for file web/xliff/en.xlf in zh-Hans (#9646)
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-08 23:27:55 +02:00
5683c81f27 translate: Updates for file web/xliff/en.xlf in zh_CN (#9645)
* Translate web/xliff/en.xlf in zh_CN

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

* 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-08 23:27:24 +02:00
f0477309d5 translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#9647)
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-08 23:27:12 +02: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
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
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
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
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
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
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
354 changed files with 25223 additions and 6326 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.4.4
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*))?

View File

@ -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

@ -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 .

15
.vscode/settings.json vendored
View File

@ -4,20 +4,21 @@
"asgi",
"authentik",
"authn",
"entra",
"goauthentik",
"jwks",
"kubernetes",
"oidc",
"openid",
"passwordless",
"plex",
"saml",
"totp",
"webauthn",
"traefik",
"passwordless",
"kubernetes",
"sso",
"slo",
"scim",
"slo",
"sso",
"totp",
"traefik",
"webauthn",
],
"todo-tree.tree.showCountsInTree": true,
"todo-tree.tree.showBadges": true,

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.4.4"
__version__ = "2024.4.2"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -4,7 +4,6 @@ from collections.abc import Iterable
from uuid import UUID
from django.apps import apps
from django.contrib.auth import get_user_model
from django.db.models import Model, Q, QuerySet
from django.utils.timezone import now
from django.utils.translation import gettext as _
@ -47,8 +46,6 @@ class Exporter:
def get_model_instances(self, model: type[Model]) -> QuerySet:
"""Return a queryset for `model`. Can be used to filter some
objects on some models"""
if model == get_user_model():
return model.objects.exclude_anonymous()
return model.objects.all()
def _pre_export(self, blueprint: Blueprint):

View File

@ -39,6 +39,14 @@ from authentik.core.models import (
)
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.models import LicenseUsage
from authentik.enterprise.providers.google_workspace.models import (
GoogleWorkspaceProviderGroup,
GoogleWorkspaceProviderUser,
)
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.rac.models import ConnectionToken
from authentik.events.logs import LogEvent, capture_logs
from authentik.events.models import SystemTask
@ -86,6 +94,7 @@ def excluded_models() -> list[type[Model]]:
# Classes that have other dependencies
AuthenticatedSession,
# Classes which are only internally managed
# FIXME: these shouldn't need to be explicitly listed, but rather based off of a mixin
FlowToken,
LicenseUsage,
SCIMGroup,
@ -100,6 +109,10 @@ def excluded_models() -> list[type[Model]]:
WebAuthnDeviceType,
SCIMSourceUser,
SCIMSourceGroup,
GoogleWorkspaceProviderUser,
GoogleWorkspaceProviderGroup,
MicrosoftEntraProviderUser,
MicrosoftEntraProviderGroup,
)

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())]},
}

View File

@ -63,8 +63,12 @@ class ProviderFilter(FilterSet):
"""Filter for providers"""
application__isnull = BooleanFilter(method="filter_application__isnull")
backchannel_only = BooleanFilter(
method="filter_backchannel_only",
backchannel = BooleanFilter(
method="filter_backchannel",
label=_(
"When not set all providers are returned. When set to true, only backchannel "
"providers are returned. When set to false, backchannel providers are excluded"
),
)
def filter_application__isnull(self, queryset: QuerySet, name, value):
@ -75,8 +79,9 @@ class ProviderFilter(FilterSet):
| Q(application__isnull=value)
)
def filter_backchannel_only(self, queryset: QuerySet, name, value):
"""Only return backchannel providers"""
def filter_backchannel(self, queryset: QuerySet, name, value):
"""By default all providers are returned. When set to true, only backchannel providers are
returned. When set to false, backchannel providers are excluded"""
return queryset.filter(is_backchannel=value)

View File

@ -45,13 +45,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")

View File

@ -14,7 +14,6 @@ from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import PassiveSerializer
from authentik.rbac.filters import ObjectFilter
class DeleteAction(Enum):
@ -54,7 +53,7 @@ class UsedByMixin:
@extend_schema(
responses={200: UsedBySerializer(many=True)},
)
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
@action(detail=True, pagination_class=None, filter_backends=[])
def used_by(self, request: Request, *args, **kwargs) -> Response:
"""Get a list of all objects that use this object"""
model: Model = self.get_object()

View File

@ -408,7 +408,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
filterset_class = UsersFilter
def get_queryset(self):
base_qs = User.objects.all().exclude_anonymous()
base_qs = User.objects.all()
if self.serializer_class(context={"request": self.request})._should_include_groups:
base_qs = base_qs.prefetch_related("ak_groups")
return base_qs

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

@ -10,7 +10,7 @@ from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.db.models import Count
import authentik.core.models
import authentik.lib.models
import authentik.lib.validators
def migrate_sessions(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
@ -160,7 +160,7 @@ class Migration(migrations.Migration):
field=models.TextField(
blank=True,
default="",
validators=[authentik.lib.models.DomainlessFormattedURLValidator()],
validators=[authentik.lib.validators.DomainlessFormattedURLValidator()],
),
),
migrations.RunPython(

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

@ -0,0 +1,23 @@
# Generated by Django 5.0.4 on 2024-04-23 16:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_core", "0035_alter_group_options_and_more"),
]
operations = [
migrations.AddField(
model_name="group",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
migrations.AddField(
model_name="user",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -22,16 +22,18 @@ 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
from authentik.lib.models import (
CreatedUpdatedModel,
DomainlessFormattedURLValidator,
SerializerModel,
SoftDeleteModel,
SoftDeleteQuerySet,
)
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.validators import DomainlessFormattedURLValidator
from authentik.policies.models import PolicyBindingModel
from authentik.tenants.models import DEFAULT_TOKEN_DURATION, DEFAULT_TOKEN_LENGTH
from authentik.tenants.utils import get_current_tenant, get_unique_identifier
@ -96,7 +98,7 @@ class UserTypes(models.TextChoices):
INTERNAL_SERVICE_ACCOUNT = "internal_service_account"
class Group(SerializerModel):
class Group(SoftDeleteModel, SerializerModel):
"""Group model which supports a basic hierarchy and has attributes"""
group_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
@ -186,31 +188,21 @@ class Group(SerializerModel):
]
class UserQuerySet(models.QuerySet):
"""User queryset"""
def exclude_anonymous(self):
"""Exclude anonymous user"""
return self.exclude(**{User.USERNAME_FIELD: settings.ANONYMOUS_USER_NAME})
class UserManager(DjangoUserManager):
"""User manager that doesn't assign is_superuser and is_staff"""
def get_queryset(self):
"""Create special user queryset"""
return UserQuerySet(self.model, using=self._db)
return SoftDeleteQuerySet(self.model, using=self._db).exclude(
**{User.USERNAME_FIELD: settings.ANONYMOUS_USER_NAME}
)
def create_user(self, username, email=None, password=None, **extra_fields):
"""User manager that doesn't assign is_superuser and is_staff"""
return self._create_user(username, email, password, **extra_fields)
def exclude_anonymous(self) -> QuerySet:
"""Exclude anonymous user"""
return self.get_queryset().exclude_anonymous()
class User(SerializerModel, GuardianUserMixin, AbstractUser):
class User(SoftDeleteModel, SerializerModel, GuardianUserMixin, AbstractUser):
"""authentik User model, based on django's contrib auth user model."""
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)

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

@ -13,8 +13,9 @@ from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
Token,
TokenIntents,
User,
)
from authentik.core.tests.utils import create_test_admin_user, create_test_user
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id
@ -23,7 +24,7 @@ class TestTokenAPI(APITestCase):
def setUp(self) -> None:
super().setUp()
self.user = create_test_user()
self.user = User.objects.create(username="testuser")
self.admin = create_test_admin_user()
self.client.force_login(self.user)
@ -153,24 +154,6 @@ class TestTokenAPI(APITestCase):
self.assertEqual(token.expiring, True)
self.assertNotEqual(token.expires.timestamp(), expires.timestamp())
def test_token_change_user(self):
"""Test creating a token and then changing the user"""
ident = generate_id()
response = self.client.post(reverse("authentik_api:token-list"), {"identifier": ident})
self.assertEqual(response.status_code, 201)
token = Token.objects.get(identifier=ident)
self.assertEqual(token.user, self.user)
self.assertEqual(token.intent, TokenIntents.INTENT_API)
self.assertEqual(token.expiring, True)
self.assertTrue(self.user.has_perm("authentik_core.view_token_key", token))
response = self.client.put(
reverse("authentik_api:token-detail", kwargs={"identifier": ident}),
data={"identifier": "user_token_poc_v3", "intent": "api", "user": self.admin.pk},
)
self.assertEqual(response.status_code, 400)
token.refresh_from_db()
self.assertEqual(token.user, self.user)
def test_list(self):
"""Test Token List (Test normal authentication)"""
Token.objects.all().delete()

View File

@ -36,7 +36,6 @@ 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
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -267,7 +266,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
@action(detail=True, pagination_class=None, filter_backends=[])
def view_certificate(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs certificate and log access"""
certificate: CertificateKeyPair = self.get_object()
@ -297,7 +296,7 @@ class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
],
responses={200: CertificateDataSerializer(many=False)},
)
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
@action(detail=True, pagination_class=None, filter_backends=[])
def view_private_key(self, request: Request, pk: str) -> Response:
"""Return certificate-key pairs private key and log access"""
certificate: CertificateKeyPair = self.get_object()

View File

@ -214,46 +214,6 @@ class TestCrypto(APITestCase):
self.assertEqual(200, response.status_code)
self.assertIn("Content-Disposition", response)
def test_certificate_download_denied(self):
"""Test certificate export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-certificate",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)
def test_private_key_download_denied(self):
"""Test private_key export (download)"""
self.client.logout()
keypair = create_test_cert()
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-view-private-key",
kwargs={"pk": keypair.pk},
),
data={"download": True},
)
self.assertEqual(403, response.status_code)
def test_used_by(self):
"""Test used_by endpoint"""
self.client.force_login(create_test_admin_user())
@ -286,26 +246,6 @@ class TestCrypto(APITestCase):
],
)
def test_used_by_denied(self):
"""Test used_by endpoint"""
self.client.logout()
keypair = create_test_cert()
OAuth2Provider.objects.create(
name=generate_id(),
client_id="test",
client_secret=generate_key(),
authorization_flow=create_test_flow(),
redirect_uris="http://localhost",
signing_key=keypair,
)
response = self.client.get(
reverse(
"authentik_api:certificatekeypair-used-by",
kwargs={"pk": keypair.pk},
)
)
self.assertEqual(403, response.status_code)
def test_discovery(self):
"""Test certificate discovery"""
name = generate_id()

View File

@ -132,7 +132,7 @@ class LicenseKey:
@staticmethod
def base_user_qs() -> QuerySet:
"""Base query set for all users"""
return User.objects.all().exclude_anonymous().exclude(is_active=False)
return User.objects.all().exclude(is_active=False)
@staticmethod
def get_default_user_count():

View File

@ -0,0 +1,33 @@
"""GoogleWorkspaceProviderGroup API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup
class GoogleWorkspaceProviderGroupSerializer(SourceSerializer):
"""GoogleWorkspaceProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
class Meta:
model = GoogleWorkspaceProviderGroup
fields = [
"id",
"group",
"group_obj",
]
class GoogleWorkspaceProviderGroupViewSet(UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProviderGroup Viewset"""
queryset = GoogleWorkspaceProviderGroup.objects.all().select_related("group")
serializer_class = GoogleWorkspaceProviderGroupSerializer
filterset_fields = ["provider__id", "group__name", "group__group_uuid"]
search_fields = ["provider__name", "group__name"]
ordering = ["group__name"]

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 GoogleWorkspaceProviderMappingSerializer(PropertyMappingSerializer):
"""GoogleWorkspaceProviderMapping Serializer"""
class Meta:
model = GoogleWorkspaceProviderMapping
fields = PropertyMappingSerializer.Meta.fields
class GoogleWorkspaceProviderMappingFilter(FilterSet):
"""Filter for GoogleWorkspaceProviderMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = GoogleWorkspaceProviderMapping
fields = "__all__"
class GoogleWorkspaceProviderMappingViewSet(UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProviderMapping Viewset"""
queryset = GoogleWorkspaceProviderMapping.objects.all()
serializer_class = GoogleWorkspaceProviderMappingSerializer
filterset_class = GoogleWorkspaceProviderMappingFilter
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 GoogleWorkspaceProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""GoogleWorkspaceProvider 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 GoogleWorkspaceProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProvider Viewset"""
queryset = GoogleWorkspaceProvider.objects.all()
serializer_class = GoogleWorkspaceProviderSerializer
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,33 @@
"""GoogleWorkspaceProviderUser API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser
class GoogleWorkspaceProviderUserSerializer(SourceSerializer):
"""GoogleWorkspaceProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
class Meta:
model = GoogleWorkspaceProviderUser
fields = [
"id",
"user",
"user_obj",
]
class GoogleWorkspaceProviderUserViewSet(UsedByMixin, ModelViewSet):
"""GoogleWorkspaceProviderUser Viewset"""
queryset = GoogleWorkspaceProviderUser.objects.all().select_related("user")
serializer_class = GoogleWorkspaceProviderUserSerializer
filterset_fields = ["provider__id", "user__username", "user__id"]
search_fields = ["provider__name", "user__username"]
ordering = ["user__username"]

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,74 @@
from django.db.models import Model
from django.http import HttpResponseBadRequest, 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 (
BadRequestSyncException,
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(request.body, exc.response.status, exc)
raise TransientSyncException(f"Failed to send request: {str(exc)}") from exc
except HttpError as exc:
self._response_handle_status_code(request.body, 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, request: dict, 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
if status_code == HttpResponseBadRequest.status_code:
raise BadRequestSyncException("Bad request", request) 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 BadRequestSyncException(f"Invalid email domain: {email}")

View File

@ -0,0 +1,233 @@
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 (
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.sync.outgoing.models import OutgoingSyncDeleteAction
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, creating: bool) -> 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:
value = mapping.evaluate(
user=None,
request=None,
group=obj,
provider=self.provider,
creating=creating,
)
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 == OutgoingSyncDeleteAction.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, True)
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"])
)
return GoogleWorkspaceProviderGroup.objects.create(
provider=self.provider, group=group, google_id=group_data["id"]
)
else:
return 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, False)
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)
self.create_sync_members(obj, google_group)
return google_group, created
def create_sync_members(self, obj: Group, google_group: GoogleWorkspaceProviderGroup):
"""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
).values_list("google_id", flat=True)
self._patch(google_group.google_id, Direction.add, connections)
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 (
GoogleWorkspaceProviderMapping,
GoogleWorkspaceProviderUser,
)
from authentik.events.models import Event, EventAction
from authentik.lib.sync.outgoing.exceptions import (
ObjectExistsSyncException,
StopSync,
TransientSyncException,
)
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
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, creating: bool) -> 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:
value = mapping.evaluate(
user=obj,
request=None,
provider=self.provider,
creating=creating,
)
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 == OutgoingSyncDeleteAction.DELETE:
response = self._request(
self.directory_service.users().delete(userKey=google_user.google_id)
)
elif self.provider.user_delete_action == OutgoingSyncDeleteAction.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, True)
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
return GoogleWorkspaceProviderUser.objects.create(
provider=self.provider, user=user, google_id=user.email
)
except TransientSyncException as exc:
raise exc
else:
return 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, False)
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 @@
# Generated by Django 5.0.6 on 2024-05-09 12:57
import django.db.models.deletion
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("authentik_providers_google_workspace", "0001_initial"),
(
"authentik_providers_google_workspace",
"0002_alter_googleworkspaceprovidergroup_options_and_more",
),
]
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")},
"verbose_name": "Google Workspace Provider Group",
"verbose_name_plural": "Google Workspace Provider Groups",
},
),
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")},
"verbose_name": "Google Workspace Provider User",
"verbose_name_plural": "Google Workspace Provider Users",
},
),
]

View File

@ -0,0 +1,27 @@
# Generated by Django 5.0.6 on 2024-05-08 14:35
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_google_workspace", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="googleworkspaceprovidergroup",
options={
"verbose_name": "Google Workspace Provider Group",
"verbose_name_plural": "Google Workspace Provider Groups",
},
),
migrations.AlterModelOptions(
name="googleworkspaceprovideruser",
options={
"verbose_name": "Google Workspace Provider User",
"verbose_name_plural": "Google Workspace Provider Users",
},
),
]

View File

@ -0,0 +1,191 @@
"""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.models import SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, 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 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=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE
)
group_delete_action = models.TextField(
choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.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 (
GoogleWorkspaceProviderSerializer,
)
return GoogleWorkspaceProviderSerializer
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 (
GoogleWorkspaceProviderMappingSerializer,
)
return GoogleWorkspaceProviderMappingSerializer
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(SerializerModel):
"""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)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.users import (
GoogleWorkspaceProviderUserSerializer,
)
return GoogleWorkspaceProviderUserSerializer
class Meta:
verbose_name = _("Google Workspace Provider User")
verbose_name_plural = _("Google Workspace Provider Users")
unique_together = (("google_id", "user", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(SerializerModel):
"""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)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.groups import (
GoogleWorkspaceProviderGroupSerializer,
)
return GoogleWorkspaceProviderGroupSerializer
class Meta:
verbose_name = _("Google Workspace Provider Group")
verbose_name_plural = _("Google Workspace Provider Groups")
unique_together = (("google_id", "group", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider 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,37 @@
"""Google Provider tasks"""
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProvider
from authentik.events.system_tasks import SystemTask
from authentik.lib.sync.outgoing.exceptions import TransientSyncException
from authentik.lib.sync.outgoing.tasks import SyncTasks
from authentik.root.celery import CELERY_APP
sync_tasks = SyncTasks(GoogleWorkspaceProvider)
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
def google_workspace_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@CELERY_APP.task(
base=SystemTask, bind=True, autoretry_for=(TransientSyncException,), retry_backoff=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(autoretry_for=(TransientSyncException,), retry_backoff=True)
def google_workspace_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
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 (
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.sync.outgoing.models import OutgoingSyncDeleteAction
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 = OutgoingSyncDeleteAction.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 (
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.sync.outgoing.models import OutgoingSyncDeleteAction
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 = OutgoingSyncDeleteAction.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 = OutgoingSyncDeleteAction.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,21 @@
"""google provider urls"""
from authentik.enterprise.providers.google_workspace.api.groups import (
GoogleWorkspaceProviderGroupViewSet,
)
from authentik.enterprise.providers.google_workspace.api.property_mappings import (
GoogleWorkspaceProviderMappingViewSet,
)
from authentik.enterprise.providers.google_workspace.api.providers import (
GoogleWorkspaceProviderViewSet,
)
from authentik.enterprise.providers.google_workspace.api.users import (
GoogleWorkspaceProviderUserViewSet,
)
api_urlpatterns = [
("providers/google_workspace", GoogleWorkspaceProviderViewSet),
("providers/google_workspace_users", GoogleWorkspaceProviderUserViewSet),
("providers/google_workspace_groups", GoogleWorkspaceProviderGroupViewSet),
("propertymappings/provider/google_workspace", GoogleWorkspaceProviderMappingViewSet),
]

View File

@ -0,0 +1,33 @@
"""MicrosoftEntraProviderGroup API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup
class MicrosoftEntraProviderGroupSerializer(SourceSerializer):
"""MicrosoftEntraProviderGroup Serializer"""
group_obj = UserGroupSerializer(source="group", read_only=True)
class Meta:
model = MicrosoftEntraProviderGroup
fields = [
"id",
"group",
"group_obj",
]
class MicrosoftEntraProviderGroupViewSet(UsedByMixin, ModelViewSet):
"""MicrosoftEntraProviderGroup Viewset"""
queryset = MicrosoftEntraProviderGroup.objects.all().select_related("group")
serializer_class = MicrosoftEntraProviderGroupSerializer
filterset_fields = ["provider__id", "group__name", "group__group_uuid"]
search_fields = ["provider__name", "group__name"]
ordering = ["group__name"]

View File

@ -0,0 +1,39 @@
"""microsoft 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.microsoft_entra.models import MicrosoftEntraProviderMapping
class MicrosoftEntraProviderMappingSerializer(PropertyMappingSerializer):
"""MicrosoftEntraProviderMapping Serializer"""
class Meta:
model = MicrosoftEntraProviderMapping
fields = PropertyMappingSerializer.Meta.fields
class MicrosoftEntraProviderMappingFilter(FilterSet):
"""Filter for MicrosoftEntraProviderMapping"""
managed = extend_schema_field(OpenApiTypes.STR)(AllValuesMultipleFilter(field_name="managed"))
class Meta:
model = MicrosoftEntraProviderMapping
fields = "__all__"
class MicrosoftEntraProviderMappingViewSet(UsedByMixin, ModelViewSet):
"""MicrosoftEntraProviderMapping Viewset"""
queryset = MicrosoftEntraProviderMapping.objects.all()
serializer_class = MicrosoftEntraProviderMappingSerializer
filterset_class = MicrosoftEntraProviderMappingFilter
search_fields = ["name"]
ordering = ["name"]

View File

@ -0,0 +1,52 @@
"""Microsoft 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.microsoft_entra.models import MicrosoftEntraProvider
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync
from authentik.lib.sync.outgoing.api import OutgoingSyncProviderStatusMixin
class MicrosoftEntraProviderSerializer(EnterpriseRequiredMixin, ProviderSerializer):
"""MicrosoftEntraProvider Serializer"""
class Meta:
model = MicrosoftEntraProvider
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",
"client_id",
"client_secret",
"tenant_id",
"exclude_users_service_account",
"filter_group",
"user_delete_action",
"group_delete_action",
]
extra_kwargs = {}
class MicrosoftEntraProviderViewSet(OutgoingSyncProviderStatusMixin, UsedByMixin, ModelViewSet):
"""MicrosoftEntraProvider Viewset"""
queryset = MicrosoftEntraProvider.objects.all()
serializer_class = MicrosoftEntraProviderSerializer
filterset_fields = [
"name",
"exclude_users_service_account",
"filter_group",
]
search_fields = ["name"]
ordering = ["name"]
sync_single_task = microsoft_entra_sync

View File

@ -0,0 +1,33 @@
"""MicrosoftEntraProviderUser API Views"""
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser
class MicrosoftEntraProviderUserSerializer(SourceSerializer):
"""MicrosoftEntraProviderUser Serializer"""
user_obj = GroupMemberSerializer(source="user", read_only=True)
class Meta:
model = MicrosoftEntraProviderUser
fields = [
"id",
"user",
"user_obj",
]
class MicrosoftEntraProviderUserViewSet(UsedByMixin, ModelViewSet):
"""MicrosoftEntraProviderUser Viewset"""
queryset = MicrosoftEntraProviderUser.objects.all().select_related("user")
serializer_class = MicrosoftEntraProviderUserSerializer
filterset_fields = ["provider__id", "user__username", "user__id"]
search_fields = ["provider__name", "user__username"]
ordering = ["user__username"]

View File

@ -0,0 +1,9 @@
from authentik.enterprise.apps import EnterpriseConfig
class AuthentikEnterpriseProviderMicrosoftEntraConfig(EnterpriseConfig):
name = "authentik.enterprise.providers.microsoft_entra"
label = "authentik_providers_microsoft_entra"
verbose_name = "authentik Enterprise.Providers.Microsoft Entra"
default = True

View File

@ -0,0 +1,100 @@
from asyncio import run
from collections.abc import Coroutine
from typing import Any
from azure.core.exceptions import (
ClientAuthenticationError,
ServiceRequestError,
ServiceResponseError,
)
from azure.identity.aio import ClientSecretCredential
from django.db.models import Model
from django.http import HttpResponseBadRequest, HttpResponseNotFound
from kiota_abstractions.api_error import APIError
from kiota_authentication_azure.azure_identity_authentication_provider import (
AzureIdentityAuthenticationProvider,
)
from kiota_http.kiota_client_factory import KiotaClientFactory
from msgraph.generated.models.o_data_errors.o_data_error import ODataError
from msgraph.graph_request_adapter import GraphRequestAdapter, options
from msgraph.graph_service_client import GraphServiceClient
from msgraph_core import GraphClientFactory
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.lib.sync.outgoing import HTTP_CONFLICT
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.exceptions import (
BadRequestSyncException,
NotFoundSyncException,
ObjectExistsSyncException,
StopSync,
TransientSyncException,
)
def get_request_adapter(
credentials: ClientSecretCredential, scopes: list[str] | None = None
) -> GraphRequestAdapter:
if scopes:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials, scopes=scopes)
else:
auth_provider = AzureIdentityAuthenticationProvider(credentials=credentials)
return GraphRequestAdapter(
auth_provider=auth_provider,
client=GraphClientFactory.create_with_default_middleware(
options=options, client=KiotaClientFactory.get_default_client()
),
)
class MicrosoftEntraSyncClient[TModel: Model, TConnection: Model, TSchema: dict](
BaseOutgoingSyncClient[TModel, TConnection, TSchema, MicrosoftEntraProvider]
):
"""Base client for syncing to microsoft entra"""
domains: list
def __init__(self, provider: MicrosoftEntraProvider) -> None:
super().__init__(provider)
self.credentials = provider.microsoft_credentials()
self.__prefetch_domains()
@property
def client(self):
return GraphServiceClient(request_adapter=get_request_adapter(**self.credentials))
def _request[T](self, request: Coroutine[Any, Any, T]) -> T:
try:
return run(request)
except ClientAuthenticationError as exc:
raise StopSync(exc, None, None) from exc
except ODataError as exc:
raise StopSync(exc, None, None) from exc
except (ServiceRequestError, ServiceResponseError) as exc:
raise TransientSyncException("Failed to sent request") from exc
except APIError as exc:
if exc.response_status_code == HttpResponseNotFound.status_code:
raise NotFoundSyncException("Object not found") from exc
if exc.response_status_code == HttpResponseBadRequest.status_code:
raise BadRequestSyncException("Bad request", exc.response_headers) from exc
if exc.response_status_code == HTTP_CONFLICT:
raise ObjectExistsSyncException("Object exists", exc.response_headers) from exc
raise exc
def __prefetch_domains(self):
self.domains = []
organizations = self._request(self.client.organization.get())
next_link = True
while next_link:
for org in organizations.value:
self.domains.extend([x.name for x in org.verified_domains])
next_link = organizations.odata_next_link
if not next_link:
break
organizations = self._request(self.client.organization.with_url(next_link).get())
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 BadRequestSyncException(f"Invalid email domain: {email}")

View File

@ -0,0 +1,241 @@
from deepmerge import always_merger
from django.db import transaction
from msgraph.generated.groups.groups_request_builder import GroupsRequestBuilder
from msgraph.generated.models.group import Group as MSGroup
from msgraph.generated.models.reference_create import ReferenceCreate
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import Group
from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderMapping,
MicrosoftEntraProviderUser,
)
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.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.lib.utils.errors import exception_to_string
class MicrosoftEntraGroupClient(
MicrosoftEntraSyncClient[Group, MicrosoftEntraProviderGroup, MSGroup]
):
"""Microsoft client for groups"""
connection_type = MicrosoftEntraProviderGroup
connection_type_query = "group"
can_discover = True
def to_schema(self, obj: Group, creating: bool) -> MSGroup:
"""Convert authentik group"""
raw_microsoft_group = {}
for mapping in (
self.provider.property_mappings_group.all().order_by("name").select_subclasses()
):
if not isinstance(mapping, MicrosoftEntraProviderMapping):
continue
try:
value = mapping.evaluate(
user=None,
request=None,
group=obj,
provider=self.provider,
creating=creating,
)
if value is None:
continue
always_merger.merge(raw_microsoft_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_microsoft_group:
raise StopSync(ValueError("No group mappings configured"), obj)
try:
return MSGroup(**raw_microsoft_group)
except TypeError as exc:
raise StopSync(exc, obj) from exc
def delete(self, obj: Group):
"""Delete group"""
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=obj
).first()
if not microsoft_group:
self.logger.debug("Group does not exist in Microsoft, skipping")
return None
with transaction.atomic():
if self.provider.group_delete_action == OutgoingSyncDeleteAction.DELETE:
self._request(self.client.groups.by_group_id(microsoft_group.microsoft_id).delete())
microsoft_group.delete()
def create(self, group: Group):
"""Create group from scratch and create a connection object"""
microsoft_group = self.to_schema(group, True)
with transaction.atomic():
try:
response = self._request(self.client.groups.post(microsoft_group))
except ObjectExistsSyncException:
# group already exists in microsoft entra, so we can connect them manually
# for groups we need to fetch the group from microsoft as we connect on
# ID and not group email
query_params = GroupsRequestBuilder.GroupsRequestBuilderGetQueryParameters(
filter=f"displayName eq '{microsoft_group.display_name}'",
)
request_configuration = (
GroupsRequestBuilder.GroupsRequestBuilderGetRequestConfiguration(
query_parameters=query_params,
)
)
group_data = self._request(self.client.groups.get(request_configuration))
if group_data.odata_count < 1:
self.logger.warning(
"Group which could not be created also does not exist", group=group
)
return
return MicrosoftEntraProviderGroup.objects.create(
provider=self.provider, group=group, microsoft_id=group_data.value[0].id
)
else:
return MicrosoftEntraProviderGroup.objects.create(
provider=self.provider, group=group, microsoft_id=response.id
)
def update(self, group: Group, connection: MicrosoftEntraProviderGroup):
"""Update existing group"""
microsoft_group = self.to_schema(group, False)
microsoft_group.id = connection.microsoft_id
try:
return self._request(
self.client.groups.by_group_id(connection.microsoft_id).patch(microsoft_group)
)
except NotFoundSyncException:
# Resource missing is handled by self.write, which will re-create the group
raise
def write(self, obj: Group):
microsoft_group, created = super().write(obj)
self.create_sync_members(obj, microsoft_group)
return microsoft_group, created
def create_sync_members(self, obj: Group, microsoft_group: MicrosoftEntraProviderGroup):
"""Sync all members after a group was created"""
users = list(obj.users.order_by("id").values_list("id", flat=True))
connections = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user__pk__in=users
).values_list("microsoft_id", flat=True)
self._patch(microsoft_group.microsoft_id, Direction.add, connections)
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, microsoft_group_id: str, direction: Direction, members: list[str]):
for user in members:
try:
if direction == Direction.add:
request_body = ReferenceCreate(
odata_id=f"https://graph.microsoft.com/v1.0/directoryObjects/{user}",
)
self._request(
self.client.groups.by_group_id(microsoft_group_id).members.ref.post(
request_body
)
)
if direction == Direction.remove:
self._request(
self.client.groups.by_group_id(microsoft_group_id)
.members.by_directory_object_id(user)
.ref.delete()
)
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
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
if not microsoft_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
MicrosoftEntraProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("microsoft_id", flat=True)
)
if len(user_ids) < 1:
return
self._patch(microsoft_group.microsoft_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
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
if not microsoft_group:
self.logger.warning(
"could not sync group membership, group does not exist", group=group
)
return
user_ids = list(
MicrosoftEntraProviderUser.objects.filter(
user__pk__in=users_set, provider=self.provider
).values_list("microsoft_id", flat=True)
)
if len(user_ids) < 1:
return
self._patch(microsoft_group.microsoft_id, Direction.remove, user_ids)
def discover(self):
"""Iterate through all groups and connect them with authentik groups if possible"""
groups = self._request(self.client.groups.get())
next_link = True
while next_link:
for group in groups.value:
self._discover_single_group(group)
next_link = groups.odata_next_link
if not next_link:
break
groups = self._request(self.client.groups.with_url(next_link).get())
def _discover_single_group(self, group: MSGroup):
"""handle discovery of a single group"""
microsoft_name = group.unique_name
matching_authentik_group = (
self.provider.get_object_qs(Group).filter(name=microsoft_name).first()
)
if not matching_authentik_group:
return
MicrosoftEntraProviderGroup.objects.get_or_create(
provider=self.provider,
group=matching_authentik_group,
microsoft_id=group.id,
)

View File

@ -0,0 +1,150 @@
from deepmerge import always_merger
from django.db import transaction
from msgraph.generated.models.user import User as MSUser
from msgraph.generated.users.users_request_builder import UsersRequestBuilder
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.models import User
from authentik.enterprise.providers.microsoft_entra.clients.base import MicrosoftEntraSyncClient
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProviderMapping,
MicrosoftEntraProviderUser,
)
from authentik.events.models import Event, EventAction
from authentik.lib.sync.outgoing.exceptions import (
ObjectExistsSyncException,
StopSync,
TransientSyncException,
)
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.lib.utils.errors import exception_to_string
from authentik.policies.utils import delete_none_values
class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProviderUser, MSUser]):
"""Sync authentik users into microsoft entra"""
connection_type = MicrosoftEntraProviderUser
connection_type_query = "user"
can_discover = True
def to_schema(self, obj: User, creating: bool) -> MSUser:
"""Convert authentik user"""
raw_microsoft_user = {}
for mapping in self.provider.property_mappings.all().order_by("name").select_subclasses():
if not isinstance(mapping, MicrosoftEntraProviderMapping):
continue
try:
value = mapping.evaluate(
user=obj,
request=None,
provider=self.provider,
creating=creating,
)
if value is None:
continue
always_merger.merge(raw_microsoft_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_microsoft_user:
raise StopSync(ValueError("No user mappings configured"), obj)
try:
return MSUser(**delete_none_values(raw_microsoft_user))
except TypeError as exc:
raise StopSync(exc, obj) from exc
def delete(self, obj: User):
"""Delete user"""
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=obj
).first()
if not microsoft_user:
self.logger.debug("User does not exist in Microsoft, skipping")
return None
with transaction.atomic():
response = None
if self.provider.user_delete_action == OutgoingSyncDeleteAction.DELETE:
response = self._request(
self.client.users.by_user_id(microsoft_user.microsoft_id).delete()
)
elif self.provider.user_delete_action == OutgoingSyncDeleteAction.SUSPEND:
response = self._request(
self.client.users.by_user_id(microsoft_user.microsoft_id).patch(
MSUser(account_enabled=False)
)
)
microsoft_user.delete()
return response
def create(self, user: User):
"""Create user from scratch and create a connection object"""
microsoft_user = self.to_schema(user, True)
self.check_email_valid(microsoft_user.user_principal_name)
with transaction.atomic():
try:
response = self._request(self.client.users.post(microsoft_user))
except ObjectExistsSyncException:
# user already exists in microsoft entra, so we can connect them manually
query_params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters()(
filter=f"mail eq '{microsoft_user.mail}'",
)
request_configuration = (
UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=query_params,
)
)
user_data = self._request(self.client.users.get(request_configuration))
if user_data.odata_count < 1:
self.logger.warning(
"User which could not be created also does not exist", user=user
)
return
return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, user=user, microsoft_id=user_data.value[0].id
)
except TransientSyncException as exc:
raise exc
else:
return MicrosoftEntraProviderUser.objects.create(
provider=self.provider, user=user, microsoft_id=response.id
)
def update(self, user: User, connection: MicrosoftEntraProviderUser):
"""Update existing user"""
microsoft_user = self.to_schema(user, False)
self.check_email_valid(microsoft_user.user_principal_name)
self._request(self.client.users.by_user_id(connection.microsoft_id).patch(microsoft_user))
def discover(self):
"""Iterate through all users and connect them with authentik users if possible"""
users = self._request(self.client.users.get())
next_link = True
while next_link:
for user in users.value:
self._discover_single_user(user)
next_link = users.odata_next_link
if not next_link:
break
users = self._request(self.client.users.with_url(next_link).get())
def _discover_single_user(self, user: MSUser):
"""handle discovery of a single user"""
matching_authentik_user = self.provider.get_object_qs(User).filter(email=user.mail).first()
if not matching_authentik_user:
return
MicrosoftEntraProviderUser.objects.get_or_create(
provider=self.provider,
user=matching_authentik_user,
microsoft_id=user.id,
)

View File

@ -0,0 +1,165 @@
# Generated by Django 5.0.6 on 2024-05-08 14:35
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="MicrosoftEntraProviderMapping",
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": "Microsoft Entra Provider Mapping",
"verbose_name_plural": "Microsoft Entra Provider Mappings",
},
bases=("authentik_core.propertymapping",),
),
migrations.CreateModel(
name="MicrosoftEntraProvider",
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",
),
),
("client_id", models.TextField()),
("client_secret", models.TextField()),
("tenant_id", 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": "Microsoft Entra Provider",
"verbose_name_plural": "Microsoft Entra Providers",
},
bases=("authentik_core.provider", models.Model),
),
migrations.CreateModel(
name="MicrosoftEntraProviderGroup",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("microsoft_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_microsoft_entra.microsoftentraprovider",
),
),
],
options={
"verbose_name": "Microsoft Entra Provider Group",
"verbose_name_plural": "Microsoft Entra Provider Groups",
"unique_together": {("microsoft_id", "group", "provider")},
},
),
migrations.CreateModel(
name="MicrosoftEntraProviderUser",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4, editable=False, primary_key=True, serialize=False
),
),
("microsoft_id", models.TextField()),
(
"provider",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="authentik_providers_microsoft_entra.microsoftentraprovider",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"verbose_name": "Microsoft Entra Provider User",
"verbose_name_plural": "Microsoft Entra Provider User",
"unique_together": {("microsoft_id", "user", "provider")},
},
),
]

View File

@ -0,0 +1,180 @@
"""Microsoft Entra sync provider"""
from typing import Any, Self
from uuid import uuid4
from azure.identity.aio import ClientSecretCredential
from django.db import models
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import (
BackchannelProvider,
Group,
PropertyMapping,
User,
UserTypes,
)
from authentik.lib.models import SerializerModel
from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider
class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
"""Sync users from authentik into Microsoft Entra."""
client_id = models.TextField()
client_secret = models.TextField()
tenant_id = models.TextField()
exclude_users_service_account = models.BooleanField(default=False)
user_delete_action = models.TextField(
choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.DELETE
)
group_delete_action = models.TextField(
choices=OutgoingSyncDeleteAction.choices, default=OutgoingSyncDeleteAction.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.microsoft_entra.clients.users import (
MicrosoftEntraUserClient,
)
return MicrosoftEntraUserClient(self)
if issubclass(model, Group):
from authentik.enterprise.providers.microsoft_entra.clients.groups import (
MicrosoftEntraGroupClient,
)
return MicrosoftEntraGroupClient(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 microsoft_credentials(self):
return {
"credentials": ClientSecretCredential(
self.tenant_id, self.client_id, self.client_secret
)
}
@property
def component(self) -> str:
return "ak-provider-microsoft-entra-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.providers import (
MicrosoftEntraProviderSerializer,
)
return MicrosoftEntraProviderSerializer
def __str__(self):
return f"Microsoft Entra Provider {self.name}"
class Meta:
verbose_name = _("Microsoft Entra Provider")
verbose_name_plural = _("Microsoft Entra Providers")
class MicrosoftEntraProviderMapping(PropertyMapping):
"""Map authentik data to outgoing Microsoft requests"""
@property
def component(self) -> str:
return "ak-property-mapping-microsoft-entra-form"
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.property_mappings import (
MicrosoftEntraProviderMappingSerializer,
)
return MicrosoftEntraProviderMappingSerializer
def __str__(self):
return f"Microsoft Entra Provider Mapping {self.name}"
class Meta:
verbose_name = _("Microsoft Entra Provider Mapping")
verbose_name_plural = _("Microsoft Entra Provider Mappings")
class MicrosoftEntraProviderUser(SerializerModel):
"""Mapping of a user and provider to a Microsoft user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.users import (
MicrosoftEntraProviderUserSerializer,
)
return MicrosoftEntraProviderUserSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider User")
verbose_name_plural = _("Microsoft Entra Provider User")
unique_together = (("microsoft_id", "user", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}"
class MicrosoftEntraProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Microsoft group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.groups import (
MicrosoftEntraProviderGroupSerializer,
)
return MicrosoftEntraProviderGroupSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider Group")
verbose_name_plural = _("Microsoft Entra Provider Groups")
unique_together = (("microsoft_id", "group", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}"

View File

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

View File

@ -0,0 +1,16 @@
"""Microsoft provider signals"""
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.enterprise.providers.microsoft_entra.tasks import (
microsoft_entra_sync,
microsoft_entra_sync_direct,
microsoft_entra_sync_m2m,
)
from authentik.lib.sync.outgoing.signals import register_signals
register_signals(
MicrosoftEntraProvider,
task_sync_single=microsoft_entra_sync,
task_sync_direct=microsoft_entra_sync_direct,
task_sync_m2m=microsoft_entra_sync_m2m,
)

View File

@ -0,0 +1,37 @@
"""Microsoft Entra Provider tasks"""
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProvider
from authentik.events.system_tasks import SystemTask
from authentik.lib.sync.outgoing.exceptions import TransientSyncException
from authentik.lib.sync.outgoing.tasks import SyncTasks
from authentik.root.celery import CELERY_APP
sync_tasks = SyncTasks(MicrosoftEntraProvider)
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
def microsoft_entra_sync_objects(*args, **kwargs):
return sync_tasks.sync_objects(*args, **kwargs)
@CELERY_APP.task(
base=SystemTask, bind=True, autoretry_for=(TransientSyncException,), retry_backoff=True
)
def microsoft_entra_sync(self, provider_pk: int, *args, **kwargs):
"""Run full sync for Microsoft Entra provider"""
return sync_tasks.sync_single(self, provider_pk, microsoft_entra_sync_objects)
@CELERY_APP.task()
def microsoft_entra_sync_all():
return sync_tasks.sync_all(microsoft_entra_sync)
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
def microsoft_entra_sync_direct(*args, **kwargs):
return sync_tasks.sync_signal_direct(*args, **kwargs)
@CELERY_APP.task(autoretry_for=(TransientSyncException,), retry_backoff=True)
def microsoft_entra_sync_m2m(*args, **kwargs):
return sync_tasks.sync_signal_m2m(*args, **kwargs)

View File

@ -0,0 +1,392 @@
"""Microsoft Entra Group tests"""
from unittest.mock import AsyncMock, MagicMock, patch
from azure.identity.aio import ClientSecretCredential
from django.test import TestCase
from msgraph.generated.models.group import Group as MSGroup
from msgraph.generated.models.group_collection_response import GroupCollectionResponse
from msgraph.generated.models.organization import Organization
from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse
from msgraph.generated.models.user import User as MSUser
from msgraph.generated.models.user_collection_response import UserCollectionResponse
from msgraph.generated.models.verified_domain import VerifiedDomain
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.microsoft_entra.models import (
MicrosoftEntraProvider,
MicrosoftEntraProviderGroup,
MicrosoftEntraProviderMapping,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.tenants.models import Tenant
class MicrosoftEntraGroupTests(TestCase):
"""Microsoft Entra Group tests"""
@apply_blueprint("system/providers-microsoft-entra.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: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_id(),
tenant_id=generate_id(),
exclude_users_service_account=True,
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
MicrosoftEntraProviderMapping.objects.get(
managed="goauthentik.io/providers/microsoft_entra/user"
)
)
self.provider.property_mappings_group.add(
MicrosoftEntraProviderMapping.objects.get(
managed="goauthentik.io/providers/microsoft_entra/group"
)
)
self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id())
def test_group_create(self):
"""Test group creation"""
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=generate_id())),
) as group_create,
):
group = Group.objects.create(name=uid)
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(microsoft_group)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
group_create.assert_called_once()
def test_group_create_update(self):
"""Test group updating"""
uid = generate_id()
ext_id = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=ext_id)),
) as group_create,
patch(
"msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
AsyncMock(return_value=MSGroup(id=ext_id)),
) as group_patch,
):
group = Group.objects.create(name=uid)
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(microsoft_group)
group.name = "new name"
group.save()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
group_create.assert_called_once()
group_patch.assert_called_once()
def test_group_create_delete(self):
"""Test group deletion"""
uid = generate_id()
ext_id = generate_id()
with (
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=ext_id)),
) as group_create,
patch(
"msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
AsyncMock(return_value=MSGroup(id=ext_id)),
) as group_delete,
):
group = Group.objects.create(name=uid)
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(microsoft_group)
group.delete()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
group_create.assert_called_once()
group_delete.assert_called_once()
def test_group_create_member_add(self):
"""Test group creation"""
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=uid)),
) as group_create,
patch(
"msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
AsyncMock(),
) as member_add,
):
user = create_test_user(uid)
group = Group.objects.create(name=uid)
group.users.add(user)
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(microsoft_group)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_create.assert_called_once()
group_create.assert_called_once()
member_add.assert_called_once()
self.assertEqual(
member_add.call_args[0][0].odata_id,
f"https://graph.microsoft.com/v1.0/directoryObjects/{MicrosoftEntraProviderUser.objects.filter(
provider=self.provider,
).first().microsoft_id}",
)
def test_group_create_member_remove(self):
"""Test group creation"""
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=uid)),
) as group_create,
patch(
"msgraph.generated.groups.item.members.ref.ref_request_builder.RefRequestBuilder.post",
AsyncMock(),
) as member_add,
patch(
"msgraph.generated.groups.item.members.item.ref.ref_request_builder.RefRequestBuilder.delete",
AsyncMock(),
) as member_remove,
):
user = create_test_user(uid)
group = Group.objects.create(name=uid)
group.users.add(user)
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(microsoft_group)
group.users.remove(user)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_create.assert_called_once()
group_create.assert_called_once()
member_add.assert_called_once()
self.assertEqual(
member_add.call_args[0][0].odata_id,
f"https://graph.microsoft.com/v1.0/directoryObjects/{MicrosoftEntraProviderUser.objects.filter(
provider=self.provider,
).first().microsoft_id}",
)
member_remove.assert_called_once()
def test_group_create_delete_do_nothing(self):
"""Test group deletion (delete action = do nothing)"""
self.provider.group_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
self.provider.save()
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=uid)),
) as group_create,
patch(
"msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.delete",
AsyncMock(return_value=MSGroup(id=uid)),
) as group_delete,
):
group = Group.objects.create(name=uid)
microsoft_group = MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group=group
).first()
self.assertIsNotNone(microsoft_group)
group.delete()
self.assertFalse(
MicrosoftEntraProviderGroup.objects.filter(
provider=self.provider, group__name=uid
).exists()
)
group_create.assert_called_once()
group_delete.assert_not_called()
def test_sync_task(self):
"""Test group discovery"""
uid = generate_id()
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.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
),
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.post",
AsyncMock(return_value=MSGroup(id=generate_id())),
),
patch(
"msgraph.generated.groups.item.group_item_request_builder.GroupItemRequestBuilder.patch",
AsyncMock(return_value=MSGroup(id=uid)),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
AsyncMock(
return_value=UserCollectionResponse(
value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
)
),
) as user_list,
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
AsyncMock(
return_value=GroupCollectionResponse(
value=[MSGroup(display_name=uid, unique_name=uid, id=uid)]
)
),
) as group_list,
):
microsoft_entra_sync.delay(self.provider.pk).get()
self.assertTrue(
MicrosoftEntraProviderGroup.objects.filter(
group=different_group, provider=self.provider
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_list.assert_called_once()
group_list.assert_called_once()

View File

@ -0,0 +1,337 @@
"""Microsoft Entra User tests"""
from unittest.mock import AsyncMock, MagicMock, patch
from azure.identity.aio import ClientSecretCredential
from django.test import TestCase
from msgraph.generated.models.group_collection_response import GroupCollectionResponse
from msgraph.generated.models.organization import Organization
from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse
from msgraph.generated.models.user import User as MSUser
from msgraph.generated.models.user_collection_response import UserCollectionResponse
from msgraph.generated.models.verified_domain import VerifiedDomain
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProvider,
MicrosoftEntraProviderMapping,
MicrosoftEntraProviderUser,
)
from authentik.enterprise.providers.microsoft_entra.tasks import microsoft_entra_sync
from authentik.events.models import Event, EventAction
from authentik.lib.generators import generate_id
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.tenants.models import Tenant
class MicrosoftEntraUserTests(TestCase):
"""Microsoft Entra User tests"""
@apply_blueprint("system/providers-microsoft-entra.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: MicrosoftEntraProvider = MicrosoftEntraProvider.objects.create(
name=generate_id(),
client_id=generate_id(),
client_secret=generate_id(),
tenant_id=generate_id(),
exclude_users_service_account=True,
)
self.app: Application = Application.objects.create(
name=generate_id(),
slug=generate_id(),
)
self.app.backchannel_providers.add(self.provider)
self.provider.property_mappings.add(
MicrosoftEntraProviderMapping.objects.get(
managed="goauthentik.io/providers/microsoft_entra/user"
)
)
self.provider.property_mappings_group.add(
MicrosoftEntraProviderMapping.objects.get(
managed="goauthentik.io/providers/microsoft_entra/group"
)
)
self.creds = ClientSecretCredential(generate_id(), generate_id(), generate_id())
def test_user_create(self):
"""Test user creation"""
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(microsoft_user)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_create.assert_called_once()
def test_user_create_update(self):
"""Test user updating"""
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_patch,
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(microsoft_user)
user.name = "new name"
user.save()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_create.assert_called_once()
user_patch.assert_called_once()
def test_user_create_delete(self):
"""Test user deletion"""
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
AsyncMock(),
) as user_delete,
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(microsoft_user)
user.delete()
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_create.assert_called_once()
user_delete.assert_called_once()
def test_user_create_delete_suspend(self):
"""Test user deletion (delete action = Suspend)"""
self.provider.user_delete_action = OutgoingSyncDeleteAction.SUSPEND
self.provider.save()
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_patch,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
AsyncMock(),
) as user_delete,
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(microsoft_user)
user.delete()
self.assertFalse(
MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user__username=uid
).exists()
)
user_create.assert_called_once()
user_patch.assert_called_once()
self.assertFalse(user_patch.call_args[0][0].account_enabled)
user_delete.assert_not_called()
def test_user_create_delete_do_nothing(self):
"""Test user deletion (delete action = do nothing)"""
self.provider.user_delete_action = OutgoingSyncDeleteAction.DO_NOTHING
self.provider.save()
uid = generate_id()
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.post",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_create,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
) as user_patch,
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.delete",
AsyncMock(),
) as user_delete,
):
user = User.objects.create(
username=uid,
name=f"{uid} {uid}",
email=f"{uid}@goauthentik.io",
)
microsoft_user = MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user=user
).first()
self.assertIsNotNone(microsoft_user)
user.delete()
self.assertFalse(
MicrosoftEntraProviderUser.objects.filter(
provider=self.provider, user__username=uid
).exists()
)
user_create.assert_called_once()
user_patch.assert_not_called()
user_delete.assert_not_called()
def test_sync_task(self):
"""Test user discovery"""
uid = generate_id()
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.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"msgraph.generated.users.item.user_item_request_builder.UserItemRequestBuilder.patch",
AsyncMock(return_value=MSUser(id=generate_id())),
),
patch(
"msgraph.generated.users.users_request_builder.UsersRequestBuilder.get",
AsyncMock(
return_value=UserCollectionResponse(
value=[MSUser(mail=f"{uid}@goauthentik.io", id=uid)]
)
),
) as user_list,
patch(
"msgraph.generated.groups.groups_request_builder.GroupsRequestBuilder.get",
AsyncMock(return_value=GroupCollectionResponse(value=[])),
),
):
microsoft_entra_sync.delay(self.provider.pk).get()
self.assertTrue(
MicrosoftEntraProviderUser.objects.filter(
user=different_user, provider=self.provider
).exists()
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_list.assert_called_once()

View File

@ -0,0 +1,21 @@
"""microsoft provider urls"""
from authentik.enterprise.providers.microsoft_entra.api.groups import (
MicrosoftEntraProviderGroupViewSet,
)
from authentik.enterprise.providers.microsoft_entra.api.property_mappings import (
MicrosoftEntraProviderMappingViewSet,
)
from authentik.enterprise.providers.microsoft_entra.api.providers import (
MicrosoftEntraProviderViewSet,
)
from authentik.enterprise.providers.microsoft_entra.api.users import (
MicrosoftEntraProviderUserViewSet,
)
api_urlpatterns = [
("providers/microsoft_entra", MicrosoftEntraProviderViewSet),
("providers/microsoft_entra_users", MicrosoftEntraProviderUserViewSet),
("providers/microsoft_entra_groups", MicrosoftEntraProviderGroupViewSet),
("propertymappings/provider/microsoft_entra", MicrosoftEntraProviderMappingViewSet),
]

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ from django.db import migrations, models
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
import authentik.events.models
import authentik.lib.models
import authentik.lib.validators
from authentik.lib.migrations import progress_bar
@ -377,7 +377,7 @@ class Migration(migrations.Migration):
model_name="notificationtransport",
name="webhook_url",
field=models.TextField(
blank=True, validators=[authentik.lib.models.DomainlessURLValidator()]
blank=True, validators=[authentik.lib.validators.DomainlessURLValidator()]
),
),
]

View File

@ -41,10 +41,11 @@ from authentik.events.utils import (
sanitize_dict,
sanitize_item,
)
from authentik.lib.models import DomainlessURLValidator, SerializerModel
from authentik.lib.models import SerializerModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.time import timedelta_from_string
from authentik.lib.validators import DomainlessURLValidator
from authentik.policies.models import PolicyBindingModel
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.email.utils import TemplateEmailMessage

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:

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

@ -33,7 +33,6 @@ from authentik.lib.utils.file import (
)
from authentik.lib.views import bad_request_message
from authentik.rbac.decorators import permission_required
from authentik.rbac.filters import ObjectFilter
LOGGER = get_logger()
@ -278,7 +277,7 @@ class FlowViewSet(UsedByMixin, ModelViewSet):
400: OpenApiResponse(description="Flow not applicable"),
},
)
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
@action(detail=True, pagination_class=None, filter_backends=[])
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

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,8 @@ def get_logger_config():
"fsevents": "WARNING",
"uvicorn": "WARNING",
"gunicorn": "INFO",
"requests_mock": "WARNING",
"hpack": "WARNING",
}
for handler_name, level in handler_level_map.items():
base_config["loggers"][handler_name] = {

View File

@ -1,13 +1,16 @@
"""Generic models"""
import re
from typing import Any
from django.core.validators import URLValidator
from django.db import models
from django.utils.regex_helper import _lazy_re_compile
from django.dispatch import Signal
from django.utils import timezone
from model_utils.managers import InheritanceManager
from rest_framework.serializers import BaseSerializer
pre_soft_delete = Signal()
post_soft_delete = Signal()
class SerializerModel(models.Model):
"""Base Abstract Model which has a serializer"""
@ -51,46 +54,57 @@ class InheritanceForeignKey(models.ForeignKey):
forward_related_accessor_class = InheritanceForwardManyToOneDescriptor
class DomainlessURLValidator(URLValidator):
"""Subclass of URLValidator which doesn't check the domain
(to allow hostnames without domain)"""
class SoftDeleteQuerySet(models.QuerySet):
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.host_re = "(" + self.hostname_re + self.domain_re + "|localhost)"
self.regex = _lazy_re_compile(
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,
def delete(self):
for obj in self.all():
obj.delete()
def hard_delete(self):
return super().delete()
class SoftDeleteManager(models.Manager):
def get_queryset(self):
return SoftDeleteQuerySet(self.model, using=self._db).filter(deleted_at__isnull=True)
class DeletedSoftDeleteManager(models.Manager):
def get_queryset(self):
return super().get_queryset().exclude(deleted_at__isnull=True)
class SoftDeleteModel(models.Model):
"""Model which doesn't fully delete itself, but rather saved the delete status
so cleanup events can run."""
deleted_at = models.DateTimeField(blank=True, null=True)
objects = SoftDeleteManager()
deleted = DeletedSoftDeleteManager()
class Meta:
abstract = True
@property
def is_deleted(self):
return self.deleted_at is not None
def delete(self, using: Any = ..., keep_parents: bool = ...) -> tuple[int, dict[str, int]]:
pre_soft_delete.send(sender=self.__class__, instance=self)
now = timezone.now()
self.deleted_at = now
self.save(
update_fields=[
"deleted_at",
]
)
self.schemes = ["http", "https", "blank"] + list(self.schemes)
post_soft_delete.send(sender=self.__class__, instance=self)
return tuple()
def __call__(self, value: str):
# Check if the scheme is valid.
scheme = value.split("://")[0].lower()
if scheme not in self.schemes:
value = "default" + value
super().__call__(value)
class DomainlessFormattedURLValidator(DomainlessURLValidator):
"""URL validator which allows for python format strings"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.formatter_re = r"([%\(\)a-zA-Z])*"
self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
self.regex = _lazy_re_compile(
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,
)
self.schemes = ["http", "https", "blank"] + list(self.schemes)
def force_delete(self, using: Any = ...):
if not self.deleted_at:
raise models.ProtectedError("Refusing to force delete non-deleted model", {self})
return super().delete(using=using)

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,82 @@
"""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: TConnection):
"""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"""
connection = self.connection_type.objects.filter(
provider=self.provider, **{self.connection_type_query: obj}
).first()
try:
if not connection:
connection = self.create(obj)
return connection, True
try:
self.update(obj, connection)
return connection, False
except NotFoundSyncException:
connection.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, creating: bool) -> 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,41 @@
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 BadRequestSyncException(BaseSyncException):
"""Exception when invalid data was sent to 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,41 @@
from typing import Any, Self
from django.core.cache import cache
from django.db.models import Model, QuerySet, TextChoices
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 OutgoingSyncDeleteAction(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 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
).get(propagate=False)
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,245 @@
from collections.abc import Callable
from celery.exceptions import Retry
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 (
BadRequestSyncException,
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 BadRequestSyncException as exc:
self.logger.warning("failed to sync object", exc=exc, obj=obj)
messages.append(
LogEvent(
_(
(
"Failed to sync {object_type} {object_name} "
"due to error: {error}"
).format_map(
{
"object_type": obj._meta.verbose_name,
"object_name": str(obj),
"error": str(exc),
}
)
),
log_level="warning",
logger="",
attributes={"arguments": exc.args[1:]},
)
)
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 TransientSyncException as exc:
raise Retry() from exc
except StopSync 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 TransientSyncException as exc:
raise Retry() from exc
except StopSync 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

@ -1,5 +1,9 @@
"""Serializer validators"""
import re
from django.core.validators import URLValidator
from django.utils.regex_helper import _lazy_re_compile
from django.utils.translation import gettext_lazy as _
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import Serializer
@ -29,3 +33,48 @@ class RequiredTogetherValidator:
def __repr__(self):
return f"<{self.__class__.__name__}(fields={smart_repr(self.fields)})>"
class DomainlessURLValidator(URLValidator):
"""Subclass of URLValidator which doesn't check the domain
(to allow hostnames without domain)"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.host_re = "(" + self.hostname_re + self.domain_re + "|localhost)"
self.regex = _lazy_re_compile(
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,
)
self.schemes = ["http", "https", "blank"] + list(self.schemes)
def __call__(self, value: str):
# Check if the scheme is valid.
scheme = value.split("://")[0].lower()
if scheme not in self.schemes:
value = "default" + value
super().__call__(value)
class DomainlessFormattedURLValidator(DomainlessURLValidator):
"""URL validator which allows for python format strings"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.formatter_re = r"([%\(\)a-zA-Z])*"
self.host_re = "(" + self.formatter_re + self.hostname_re + self.domain_re + "|localhost)"
self.regex = _lazy_re_compile(
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,
)
self.schemes = ["http", "https", "blank"] + list(self.schemes)

View File

@ -23,7 +23,6 @@ from authentik.outposts.models import (
KubernetesServiceConnection,
OutpostServiceConnection,
)
from authentik.rbac.filters import ObjectFilter
class ServiceConnectionSerializer(ModelSerializer, MetaNameSerializer):
@ -89,7 +88,7 @@ class ServiceConnectionViewSet(
return Response(TypeCreateSerializer(data, many=True).data)
@extend_schema(responses={200: ServiceConnectionStateSerializer(many=False)})
@action(detail=True, pagination_class=None, filter_backends=[ObjectFilter])
@action(detail=True, pagination_class=None, filter_backends=[])
def state(self, request: Request, pk: str) -> Response:
"""Get the service connection's state"""
connection = self.get_object()

View File

@ -0,0 +1,18 @@
# Generated by Django 5.0.4 on 2024-04-23 21:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_outposts", "0021_alter_outpost_type"),
]
operations = [
migrations.AddField(
model_name="outpost",
name="deleted_at",
field=models.DateTimeField(blank=True, null=True),
),
]

View File

@ -33,7 +33,7 @@ from authentik.core.models import (
from authentik.crypto.models import CertificateKeyPair
from authentik.events.models import Event, EventAction
from authentik.lib.config import CONFIG
from authentik.lib.models import InheritanceForeignKey, SerializerModel
from authentik.lib.models import InheritanceForeignKey, SerializerModel, SoftDeleteModel
from authentik.lib.sentry import SentryIgnoredException
from authentik.lib.utils.errors import exception_to_string
from authentik.outposts.controllers.k8s.utils import get_namespace
@ -131,7 +131,7 @@ class OutpostServiceConnection(models.Model):
verbose_name = _("Outpost Service-Connection")
verbose_name_plural = _("Outpost Service-Connections")
def __str__(self) -> __version__:
def __str__(self):
return f"Outpost service connection {self.name}"
@property
@ -241,7 +241,7 @@ class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection):
return "ak-service-connection-kubernetes-form"
class Outpost(SerializerModel, ManagedModel):
class Outpost(SoftDeleteModel, SerializerModel, ManagedModel):
"""Outpost instance which manages a service user and token"""
uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True)

View File

@ -2,13 +2,14 @@
from django.core.cache import cache
from django.db.models import Model
from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
from django.db.models.signals import m2m_changed, post_save, pre_save
from django.dispatch import receiver
from structlog.stdlib import get_logger
from authentik.brands.models import Brand
from authentik.core.models import Provider
from authentik.crypto.models import CertificateKeyPair
from authentik.lib.models import post_soft_delete
from authentik.lib.utils.reflection import class_to_path
from authentik.outposts.models import Outpost, OutpostServiceConnection
from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save
@ -67,9 +68,7 @@ def post_save_update(sender, instance: Model, created: bool, **_):
outpost_post_save.delay(class_to_path(instance.__class__), instance.pk)
@receiver(pre_delete, sender=Outpost)
def pre_delete_cleanup(sender, instance: Outpost, **_):
@receiver(post_soft_delete, sender=Outpost)
def outpost_cleanup(sender, instance: Outpost, **_):
"""Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
instance.user.delete()
cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
outpost_controller.delay(instance.pk.hex, action="down", from_cache=True)
outpost_controller.delay(instance.pk.hex, action="down")

View File

@ -129,17 +129,14 @@ def outpost_controller_all():
@CELERY_APP.task(bind=True, base=SystemTask)
def outpost_controller(
self: SystemTask, outpost_pk: str, action: str = "up", from_cache: bool = False
):
def outpost_controller(self: SystemTask, outpost_pk: str, action: str = "up"):
"""Create/update/monitor/delete the deployment of an Outpost"""
logs = []
if from_cache:
outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
LOGGER.debug("Getting outpost from cache to delete")
else:
outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first()
LOGGER.debug("Getting outpost from DB")
outpost: Outpost = None
if action == "up":
outpost = Outpost.objects.filter(pk=outpost_pk).first()
elif action == "down":
outpost = Outpost.deleted.filter(pk=outpost_pk).first()
if not outpost:
LOGGER.warning("No outpost")
return
@ -155,9 +152,10 @@ def outpost_controller(
except (ControllerException, ServiceConnectionInvalid) as exc:
self.set_error(exc)
else:
if from_cache:
cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
self.set_status(TaskStatus.SUCCESSFUL, *logs)
finally:
if outpost.deleted_at:
outpost.force_delete()
@CELERY_APP.task(bind=True, base=SystemTask)

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