Compare commits

..

45 Commits

Author SHA1 Message Date
57a38c93fc cleanup and fix tests
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 22:57:41 +02:00
93b9dae178 fix logic
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 20:32:58 +02:00
589c123dc1 update in models too
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 20:19:31 +02:00
a4d9f08095 also default to entryDN for new sources
(this will also help us with a future migration to better save association with ldap sources)

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2024-04-25 20:01:27 +02:00
d856e403f8 sources/ldap: allow using entryDN as uniqueness field 2024-04-25 19:59:11 +02: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
121 changed files with 5682 additions and 3847 deletions

View File

@ -1,5 +1,5 @@
[bumpversion] [bumpversion]
current_version = 2024.4.2 current_version = 2024.4.0
tag = True tag = True
commit = 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*))? parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?

View File

@ -12,7 +12,7 @@ should_build = str(os.environ.get("DOCKER_USERNAME", None) is not None).lower()
branch_name = os.environ["GITHUB_REF"] branch_name = os.environ["GITHUB_REF"]
if os.environ.get("GITHUB_HEAD_REF", "") != "": if os.environ.get("GITHUB_HEAD_REF", "") != "":
branch_name = os.environ["GITHUB_HEAD_REF"] branch_name = os.environ["GITHUB_HEAD_REF"]
safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-").replace("'", "-") safe_branch_name = branch_name.replace("refs/heads/", "").replace("/", "-")
image_names = os.getenv("IMAGE_NAME").split(",") image_names = os.getenv("IMAGE_NAME").split(",")
image_arch = os.getenv("IMAGE_ARCH") or None image_arch = os.getenv("IMAGE_ARCH") or None

View File

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

View File

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

View File

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

View File

@ -154,18 +154,12 @@ class GroupViewSet(UsedByMixin, ModelViewSet):
pk = IntegerField(required=True) pk = IntegerField(required=True)
queryset = Group.objects.none() queryset = Group.objects.all().select_related("parent").prefetch_related("users")
serializer_class = GroupSerializer serializer_class = GroupSerializer
search_fields = ["name", "is_superuser"] search_fields = ["name", "is_superuser"]
filterset_class = GroupFilter filterset_class = GroupFilter
ordering = ["name"] ordering = ["name"]
def get_queryset(self):
base_qs = Group.objects.all().select_related("parent").prefetch_related("roles")
if self.serializer_class(context={"request": self.request})._should_include_users:
base_qs = base_qs.prefetch_related("users")
return base_qs
@extend_schema( @extend_schema(
parameters=[ parameters=[
OpenApiParameter("include_users", bool, default=True), OpenApiParameter("include_users", bool, default=True),

View File

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

View File

@ -632,7 +632,7 @@ class UserSourceConnection(SerializerModel, CreatedUpdatedModel):
raise NotImplementedError raise NotImplementedError
def __str__(self) -> str: def __str__(self) -> str:
return f"User-source connection (user={self.user_id}, source={self.source_id})" return f"User-source connection (user={self.user.username}, source={self.source.slug})"
class Meta: class Meta:
unique_together = (("user", "source"),) unique_together = (("user", "source"),)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -201,7 +201,10 @@ class ConnectionToken(ExpiringModel):
return settings return settings
def __str__(self): def __str__(self):
return f"RAC Connection token {self.session_id} to {self.provider_id}/{self.endpoint_id}" return (
f"RAC Connection token {self.session.user} to "
f"{self.endpoint.provider.name}/{self.endpoint.name}"
)
class Meta: class Meta:
verbose_name = _("RAC Connection token") verbose_name = _("RAC Connection token")

View File

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

View File

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

View File

@ -119,7 +119,7 @@ class SystemTask(TenantTask):
"task_call_kwargs": sanitize_item(kwargs), "task_call_kwargs": sanitize_item(kwargs),
"status": self._status, "status": self._status,
"messages": sanitize_item(self._messages), "messages": sanitize_item(self._messages),
"expires": now() + timedelta(hours=self.result_timeout_hours + 3), "expires": now() + timedelta(hours=self.result_timeout_hours),
"expiring": True, "expiring": True,
}, },
) )

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
if not scim_group: if not scim_group:
self.logger.debug("Group does not exist in SCIM, skipping") self.logger.debug("Group does not exist in SCIM, skipping")
return None return None
response = self._request("DELETE", f"/Groups/{scim_group.scim_id}") response = self._request("DELETE", f"/Groups/{scim_group.id}")
scim_group.delete() scim_group.delete()
return response return response
@ -89,7 +89,7 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
for user in connections: for user in connections:
members.append( members.append(
GroupMember( GroupMember(
value=user.scim_id, value=user.id,
) )
) )
if members: if members:
@ -107,19 +107,16 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
exclude_unset=True, exclude_unset=True,
), ),
) )
scim_id = response.get("id") SCIMGroup.objects.create(provider=self.provider, group=group, id=response["id"])
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMGroup.objects.create(provider=self.provider, group=group, scim_id=scim_id)
def _update(self, group: Group, connection: SCIMGroup): def _update(self, group: Group, connection: SCIMGroup):
"""Update existing group""" """Update existing group"""
scim_group = self.to_scim(group) scim_group = self.to_scim(group)
scim_group.id = connection.scim_id scim_group.id = connection.id
try: try:
return self._request( return self._request(
"PUT", "PUT",
f"/Groups/{connection.scim_id}", f"/Groups/{scim_group.id}",
json=scim_group.model_dump( json=scim_group.model_dump(
mode="json", mode="json",
exclude_unset=True, exclude_unset=True,
@ -188,13 +185,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return return
user_ids = list( user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list( SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"scim_id", flat=True "id", flat=True
) )
) )
if len(user_ids) < 1: if len(user_ids) < 1:
return return
self._patch( self._patch(
scim_group.scim_id, scim_group.id,
PatchOperation( PatchOperation(
op=PatchOp.add, op=PatchOp.add,
path="members", path="members",
@ -214,13 +211,13 @@ class SCIMGroupClient(SCIMClient[Group, SCIMGroupSchema]):
return return
user_ids = list( user_ids = list(
SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list( SCIMUser.objects.filter(user__pk__in=users_set, provider=self.provider).values_list(
"scim_id", flat=True "id", flat=True
) )
) )
if len(user_ids) < 1: if len(user_ids) < 1:
return return
self._patch( self._patch(
scim_group.scim_id, scim_group.id,
PatchOperation( PatchOperation(
op=PatchOp.remove, op=PatchOp.remove,
path="members", path="members",

View File

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

View File

@ -34,7 +34,7 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
if not scim_user: if not scim_user:
self.logger.debug("User does not exist in SCIM, skipping") self.logger.debug("User does not exist in SCIM, skipping")
return None return None
response = self._request("DELETE", f"/Users/{scim_user.scim_id}") response = self._request("DELETE", f"/Users/{scim_user.id}")
scim_user.delete() scim_user.delete()
return response return response
@ -85,18 +85,15 @@ class SCIMUserClient(SCIMClient[User, SCIMUserSchema]):
exclude_unset=True, exclude_unset=True,
), ),
) )
scim_id = response.get("id") SCIMUser.objects.create(provider=self.provider, user=user, id=response["id"])
if not scim_id or scim_id == "":
raise StopSync("SCIM Response with missing or invalid `id`")
SCIMUser.objects.create(provider=self.provider, user=user, scim_id=scim_id)
def _update(self, user: User, connection: SCIMUser): def _update(self, user: User, connection: SCIMUser):
"""Update existing user""" """Update existing user"""
scim_user = self.to_scim(user) scim_user = self.to_scim(user)
scim_user.id = connection.scim_id scim_user.id = connection.id
self._request( self._request(
"PUT", "PUT",
f"/Users/{connection.scim_id}", f"/Users/{connection.id}",
json=scim_user.model_dump( json=scim_user.model_dump(
mode="json", mode="json",
exclude_unset=True, exclude_unset=True,

View File

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

View File

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

View File

@ -1,7 +1,5 @@
"""SCIM Provider models""" """SCIM Provider models"""
from uuid import uuid4
from django.core.cache import cache from django.core.cache import cache
from django.db import models from django.db import models
from django.db.models import QuerySet from django.db.models import QuerySet
@ -99,28 +97,26 @@ class SCIMMapping(PropertyMapping):
class SCIMUser(models.Model): class SCIMUser(models.Model):
"""Mapping of a user and provider to a SCIM user ID""" """Mapping of a user and provider to a SCIM user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4) id = models.TextField(primary_key=True)
scim_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE) user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE) provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta: class Meta:
unique_together = (("scim_id", "user", "provider"),) unique_together = (("id", "user", "provider"),)
def __str__(self) -> str: def __str__(self) -> str:
return f"SCIM User {self.user_id} to {self.provider_id}" return f"SCIM User {self.user.username} to {self.provider.name}"
class SCIMGroup(models.Model): class SCIMGroup(models.Model):
"""Mapping of a group and provider to a SCIM user ID""" """Mapping of a group and provider to a SCIM user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4) id = models.TextField(primary_key=True)
scim_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE) group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE) provider = models.ForeignKey(SCIMProvider, on_delete=models.CASCADE)
class Meta: class Meta:
unique_together = (("scim_id", "group", "provider"),) unique_together = (("id", "group", "provider"),)
def __str__(self) -> str: def __str__(self) -> str:
return f"SCIM Group {self.group_id} to {self.provider_id}" return f"SCIM Group {self.group.name} to {self.provider.name}"

View File

@ -9,7 +9,7 @@ from structlog.stdlib import get_logger
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.lib.utils.reflection import class_to_path from authentik.lib.utils.reflection import class_to_path
from authentik.providers.scim.models import SCIMProvider from authentik.providers.scim.models import SCIMProvider
from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_task_wrapper from authentik.providers.scim.tasks import scim_signal_direct, scim_signal_m2m, scim_sync
LOGGER = get_logger() LOGGER = get_logger()
@ -17,7 +17,7 @@ LOGGER = get_logger()
@receiver(post_save, sender=SCIMProvider) @receiver(post_save, sender=SCIMProvider)
def post_save_provider(sender: type[Model], instance, created: bool, **_): def post_save_provider(sender: type[Model], instance, created: bool, **_):
"""Trigger sync when SCIM provider is saved""" """Trigger sync when SCIM provider is saved"""
scim_task_wrapper(instance.pk) scim_sync.delay(instance.pk)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)

View File

@ -38,23 +38,7 @@ def client_for_model(provider: SCIMProvider, model: Model) -> SCIMClient:
def scim_sync_all(): def scim_sync_all():
"""Run sync for all providers""" """Run sync for all providers"""
for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False): for provider in SCIMProvider.objects.filter(backchannel_application__isnull=False):
scim_task_wrapper(provider.pk) scim_sync.delay(provider.pk)
def scim_task_wrapper(provider_pk: int):
"""Wrap scim_sync to set the correct timeouts"""
provider: SCIMProvider = SCIMProvider.objects.filter(
pk=provider_pk, backchannel_application__isnull=False
).first()
if not provider:
return
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
soft_time_limit = (users_paginator.num_pages + groups_paginator.num_pages) * PAGE_TIMEOUT
time_limit = soft_time_limit * 1.5
return scim_sync.apply_async(
(provider.pk,), time_limit=int(time_limit), soft_time_limit=int(soft_time_limit)
)
@CELERY_APP.task(bind=True, base=SystemTask) @CELERY_APP.task(bind=True, base=SystemTask)
@ -76,7 +60,7 @@ def scim_sync(self: SystemTask, provider_pk: int) -> None:
users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE) users_paginator = Paginator(provider.get_user_qs(), PAGE_SIZE)
groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE) groups_paginator = Paginator(provider.get_group_qs(), PAGE_SIZE)
self.soft_time_limit = self.time_limit = ( self.soft_time_limit = self.time_limit = (
users_paginator.num_pages + groups_paginator.num_pages users_paginator.count + groups_paginator.count
) * PAGE_TIMEOUT ) * PAGE_TIMEOUT
with allow_join_result(): with allow_join_result():
try: try:

View File

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

View File

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

View File

@ -376,13 +376,7 @@ CELERY = {
"task_default_queue": "authentik", "task_default_queue": "authentik",
"broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")), "broker_url": CONFIG.get("broker.url") or redis_url(CONFIG.get("redis.db")),
"result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")), "result_backend": CONFIG.get("result_backend.url") or redis_url(CONFIG.get("redis.db")),
"broker_transport_options": CONFIG.get_dict_from_b64_json( "broker_transport_options": CONFIG.get_dict_from_b64_json("broker.transport_options"),
"broker.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"result_backend_transport_options": CONFIG.get_dict_from_b64_json(
"result_backend.transport_options", {"retry_policy": {"timeout": 5.0}}
),
"redis_retry_on_timeout": True,
} }
# Sentry integration # Sentry integration

View File

@ -153,7 +153,7 @@ class Migration(migrations.Migration):
( (
"object_uniqueness_field", "object_uniqueness_field",
models.TextField( models.TextField(
default="objectSid", help_text="Field which contains a unique Identifier." default="entryDN", help_text="Field which contains a unique Identifier."
), ),
), ),
("sync_groups", models.BooleanField(default=True)), ("sync_groups", models.BooleanField(default=True)),

View File

@ -88,7 +88,7 @@ class LDAPSource(Source):
help_text=_("Consider Objects matching this filter to be Groups."), help_text=_("Consider Objects matching this filter to be Groups."),
) )
object_uniqueness_field = models.TextField( object_uniqueness_field = models.TextField(
default="objectSid", help_text=_("Field which contains a unique Identifier.") default="entryDN", help_text=_("Field which contains a unique Identifier.")
) )
property_mappings_group = models.ManyToManyField( property_mappings_group = models.ManyToManyField(

View File

@ -47,6 +47,15 @@ class BaseLDAPSynchronizer:
"""UI name for the type of object this class synchronizes""" """UI name for the type of object this class synchronizes"""
raise NotImplementedError raise NotImplementedError
def get_unique_identifier(self, ldap_object: dict) -> str | None:
"""Get unique identifier"""
attributes = ldap_object.get("attributes", {})
if self._source.object_uniqueness_field in attributes:
return flatten(attributes[self._source.object_uniqueness_field])
if self._source.object_uniqueness_field in ldap_object:
return flatten(ldap_object.get(self._source.object_uniqueness_field))
return None
def sync_full(self): def sync_full(self):
"""Run full sync, this function should only be used in tests""" """Run full sync, this function should only be used in tests"""
if not settings.TEST: # noqa if not settings.TEST: # noqa
@ -134,20 +143,22 @@ class BaseLDAPSynchronizer:
cookie = None cookie = None
yield self._connection.response yield self._connection.response
def build_user_properties(self, user_dn: str, **kwargs) -> dict[str, Any]: def build_user_properties(self, user_dn: str, uniq: str, **kwargs) -> dict[str, Any]:
"""Build attributes for User object based on property mappings.""" """Build attributes for User object based on property mappings."""
props = self._build_object_properties(user_dn, self._source.property_mappings, **kwargs) props = self._build_object_properties(
user_dn, self._source.property_mappings, uniq, **kwargs
)
props.setdefault("path", self._source.get_user_path()) props.setdefault("path", self._source.get_user_path())
return props return props
def build_group_properties(self, group_dn: str, **kwargs) -> dict[str, Any]: def build_group_properties(self, group_dn: str, uniq: str, **kwargs) -> dict[str, Any]:
"""Build attributes for Group object based on property mappings.""" """Build attributes for Group object based on property mappings."""
return self._build_object_properties( return self._build_object_properties(
group_dn, self._source.property_mappings_group, **kwargs group_dn, self._source.property_mappings_group, uniq, **kwargs
) )
def _build_object_properties( def _build_object_properties(
self, object_dn: str, mappings: QuerySet, **kwargs self, object_dn: str, mappings: QuerySet, uniq: str, **kwargs
) -> dict[str, dict[Any, Any]]: ) -> dict[str, dict[Any, Any]]:
properties = {"attributes": {}} properties = {"attributes": {}}
for mapping in mappings.all().select_subclasses(): for mapping in mappings.all().select_subclasses():
@ -180,10 +191,7 @@ class BaseLDAPSynchronizer:
).save() ).save()
self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping) self._logger.warning("Mapping failed to evaluate", exc=exc, mapping=mapping)
continue continue
if self._source.object_uniqueness_field in kwargs: properties["attributes"][LDAP_UNIQUENESS] = uniq
properties["attributes"][LDAP_UNIQUENESS] = flatten(
kwargs.get(self._source.object_uniqueness_field)
)
properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn properties["attributes"][LDAP_DISTINGUISHED_NAME] = object_dn
return properties return properties

View File

@ -41,16 +41,16 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
continue continue
attributes = group.get("attributes", {}) attributes = group.get("attributes", {})
group_dn = flatten(flatten(group.get("entryDN", group.get("dn")))) group_dn = flatten(flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes: uniq = self.get_unique_identifier(group)
if not uniq:
self.message( self.message(
f"Cannot find uniqueness field in attributes: '{group_dn}'", f"Cannot find uniqueness field in attributes: '{group_dn}'",
attributes=attributes.keys(), attributes=attributes.keys(),
dn=group_dn, dn=group_dn,
) )
continue continue
uniq = flatten(attributes[self._source.object_uniqueness_field])
try: try:
defaults = self.build_group_properties(group_dn, **attributes) defaults = self.build_group_properties(group_dn, uniq, **attributes)
defaults["parent"] = self._source.sync_parent_group defaults["parent"] = self._source.sync_parent_group
if "name" not in defaults: if "name" not in defaults:
raise IntegrityError("Name was not set by propertymappings") raise IntegrityError("Name was not set by propertymappings")

View File

@ -4,7 +4,7 @@ from collections.abc import Generator
from typing import Any from typing import Any
from django.db.models import Q from django.db.models import Q
from ldap3 import SUBTREE from ldap3 import ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES, SUBTREE
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
@ -33,11 +33,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
search_base=self.base_dn_groups, search_base=self.base_dn_groups,
search_filter=self._source.group_object_filter, search_filter=self._source.group_object_filter,
search_scope=SUBTREE, search_scope=SUBTREE,
attributes=[ attributes=[ALL_ATTRIBUTES, ALL_OPERATIONAL_ATTRIBUTES],
self._source.group_membership_field,
self._source.object_uniqueness_field,
LDAP_DISTINGUISHED_NAME,
],
**kwargs, **kwargs,
) )
@ -80,7 +76,7 @@ class MembershipLDAPSynchronizer(BaseLDAPSynchronizer):
def get_group(self, group_dict: dict[str, Any]) -> Group | None: def get_group(self, group_dict: dict[str, Any]) -> Group | None:
"""Check if we fetched the group already, and if not cache it for later""" """Check if we fetched the group already, and if not cache it for later"""
group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, []) group_dn = group_dict.get("attributes", {}).get(LDAP_DISTINGUISHED_NAME, [])
group_uniq = group_dict.get("attributes", {}).get(self._source.object_uniqueness_field, []) group_uniq = self.get_unique_identifier(group_dict)
# group_uniq might be a single string or an array with (hopefully) a single string # group_uniq might be a single string or an array with (hopefully) a single string
if isinstance(group_uniq, list): if isinstance(group_uniq, list):
if len(group_uniq) < 1: if len(group_uniq) < 1:

View File

@ -43,16 +43,16 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
continue continue
attributes = user.get("attributes", {}) attributes = user.get("attributes", {})
user_dn = flatten(user.get("entryDN", user.get("dn"))) user_dn = flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes: uniq = self.get_unique_identifier(user)
if not uniq:
self.message( self.message(
f"Cannot find uniqueness field in attributes: '{user_dn}'", f"Cannot find uniqueness field in attributes: '{user_dn}'",
attributes=attributes.keys(), attributes=attributes.keys(),
dn=user_dn, dn=user_dn,
) )
continue continue
uniq = flatten(attributes[self._source.object_uniqueness_field])
try: try:
defaults = self.build_user_properties(user_dn, **attributes) defaults = self.build_user_properties(user_dn, uniq, **attributes)
self._logger.debug("Writing user with attributes", **defaults) self._logger.debug("Writing user with attributes", **defaults)
if "username" not in defaults: if "username" not in defaults:
raise IntegrityError("Username was not set by propertymappings") raise IntegrityError("Username was not set by propertymappings")

View File

@ -41,7 +41,7 @@ def mock_ad_connection(password: str) -> Connection:
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=group2,ou=groups,dc=goauthentik,dc=io", "cn=group2,ou=groups,dc=goauthentik,dc=io",
{ {
"name": "test-group", "name": "test-group2",
"objectClass": "group", "objectClass": "group",
"distinguishedName": "cn=group2,ou=groups,dc=goauthentik,dc=io", "distinguishedName": "cn=group2,ou=groups,dc=goauthentik,dc=io",
}, },
@ -61,18 +61,6 @@ def mock_ad_connection(password: str) -> Connection:
), ),
}, },
) )
# User without SID
connection.strategy.add_entry(
"cn=user1,ou=users,dc=goauthentik,dc=io",
{
"userPassword": "test1111",
"sAMAccountName": "user2_sn",
"name": "user1_sn",
"revision": 0,
"objectClass": "person",
"distinguishedName": "cn=user1,ou=users,dc=goauthentik,dc=io",
},
)
# Duplicate users # Duplicate users
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user2,ou=users,dc=goauthentik,dc=io", "cn=user2,ou=users,dc=goauthentik,dc=io",
@ -87,7 +75,7 @@ def mock_ad_connection(password: str) -> Connection:
}, },
) )
connection.strategy.add_entry( connection.strategy.add_entry(
"cn=user3,ou=users,dc=goauthentik,dc=io", "cn=user2,ou=users,dc=goauthentik,dc=io",
{ {
"userPassword": "test2222", "userPassword": "test2222",
"sAMAccountName": "user2_sn", "sAMAccountName": "user2_sn",
@ -95,7 +83,7 @@ def mock_ad_connection(password: str) -> Connection:
"revision": 0, "revision": 0,
"objectSid": "unique-test2222", "objectSid": "unique-test2222",
"objectClass": "person", "objectClass": "person",
"distinguishedName": "cn=user3,ou=users,dc=goauthentik,dc=io", "distinguishedName": "cn=user2,ou=users,dc=goauthentik,dc=io",
}, },
) )
connection.bind() connection.bind()

View File

@ -108,12 +108,7 @@ class LDAPSyncTests(TestCase):
user = User.objects.create( user = User.objects.create(
username="user0_sn", username="user0_sn",
attributes={ attributes={
"ldap_uniq": ( "ldap_uniq": "cn=user0,ou=foo,ou=users,dc=goauthentik,dc=io",
"S-117-6648368-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-"
"0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-"
"0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-"
"0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0-0"
),
"foo": "bar", "foo": "bar",
}, },
) )

View File

@ -80,7 +80,7 @@ class OAuth2Client(BaseOAuthClient):
access_token_url = self.source.source_type.access_token_url or "" access_token_url = self.source.source_type.access_token_url or ""
if self.source.source_type.urls_customizable and self.source.access_token_url: if self.source.source_type.urls_customizable and self.source.access_token_url:
access_token_url = self.source.access_token_url access_token_url = self.source.access_token_url
response = self.do_request( response = self.session.request(
"post", access_token_url, data=args, headers=self._default_headers, **request_kwargs "post", access_token_url, data=args, headers=self._default_headers, **request_kwargs
) )
response.raise_for_status() response.raise_for_status()

View File

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

View File

@ -15,10 +15,6 @@ from authentik.flows.models import Flow
from authentik.lib.utils.time import timedelta_string_validator from authentik.lib.utils.time import timedelta_string_validator
from authentik.sources.saml.processors.constants import ( from authentik.sources.saml.processors.constants import (
DSA_SHA1, DSA_SHA1,
ECDSA_SHA1,
ECDSA_SHA256,
ECDSA_SHA384,
ECDSA_SHA512,
RSA_SHA1, RSA_SHA1,
RSA_SHA256, RSA_SHA256,
RSA_SHA384, RSA_SHA384,
@ -147,7 +143,8 @@ class SAMLSource(Source):
verbose_name=_("Signing Keypair"), verbose_name=_("Signing Keypair"),
) )
digest_algorithm = models.TextField( digest_algorithm = models.CharField(
max_length=50,
choices=( choices=(
(SHA1, _("SHA1")), (SHA1, _("SHA1")),
(SHA256, _("SHA256")), (SHA256, _("SHA256")),
@ -156,16 +153,13 @@ class SAMLSource(Source):
), ),
default=SHA256, default=SHA256,
) )
signature_algorithm = models.TextField( signature_algorithm = models.CharField(
max_length=50,
choices=( choices=(
(RSA_SHA1, _("RSA-SHA1")), (RSA_SHA1, _("RSA-SHA1")),
(RSA_SHA256, _("RSA-SHA256")), (RSA_SHA256, _("RSA-SHA256")),
(RSA_SHA384, _("RSA-SHA384")), (RSA_SHA384, _("RSA-SHA384")),
(RSA_SHA512, _("RSA-SHA512")), (RSA_SHA512, _("RSA-SHA512")),
(ECDSA_SHA1, _("ECDSA-SHA1")),
(ECDSA_SHA256, _("ECDSA-SHA256")),
(ECDSA_SHA384, _("ECDSA-SHA384")),
(ECDSA_SHA512, _("ECDSA-SHA512")),
(DSA_SHA1, _("DSA-SHA1")), (DSA_SHA1, _("DSA-SHA1")),
), ),
default=RSA_SHA256, default=RSA_SHA256,

View File

@ -26,16 +26,9 @@ SAML_BINDING_REDIRECT = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect"
DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1" DSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1" RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.2
RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
# https://datatracker.ietf.org/doc/html/rfc4051#section-2.3.6
ECDSA_SHA1 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1"
ECDSA_SHA224 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha224"
ECDSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256"
ECDSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384"
ECDSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512"
SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1" SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256" SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
@ -48,11 +41,6 @@ SIGN_ALGORITHM_TRANSFORM_MAP = {
RSA_SHA256: xmlsec.constants.TransformRsaSha256, RSA_SHA256: xmlsec.constants.TransformRsaSha256,
RSA_SHA384: xmlsec.constants.TransformRsaSha384, RSA_SHA384: xmlsec.constants.TransformRsaSha384,
RSA_SHA512: xmlsec.constants.TransformRsaSha512, RSA_SHA512: xmlsec.constants.TransformRsaSha512,
ECDSA_SHA1: xmlsec.constants.TransformEcdsaSha1,
ECDSA_SHA224: xmlsec.constants.TransformEcdsaSha224,
ECDSA_SHA256: xmlsec.constants.TransformEcdsaSha256,
ECDSA_SHA384: xmlsec.constants.TransformEcdsaSha384,
ECDSA_SHA512: xmlsec.constants.TransformEcdsaSha512,
} }
DIGEST_ALGORITHM_TRANSLATION_MAP = { DIGEST_ALGORITHM_TRANSLATION_MAP = {

View File

@ -60,7 +60,7 @@ class SCIMSourceUser(SerializerModel):
unique_together = (("id", "user", "source"),) unique_together = (("id", "user", "source"),)
def __str__(self) -> str: def __str__(self) -> str:
return f"SCIM User {self.user_id} to {self.source_id}" return f"SCIM User {self.user.username} to {self.source.name}"
class SCIMSourceGroup(SerializerModel): class SCIMSourceGroup(SerializerModel):
@ -81,4 +81,4 @@ class SCIMSourceGroup(SerializerModel):
unique_together = (("id", "group", "source"),) unique_together = (("id", "group", "source"),)
def __str__(self) -> str: def __str__(self) -> str:
return f"SCIM Group {self.group_id} to {self.source_id}" return f"SCIM Group {self.group.name} to {self.source.name}"

View File

@ -2,11 +2,9 @@ from django.db.models import Model
from django.db.models.signals import pre_delete, pre_save from django.db.models.signals import pre_delete, pre_save
from django.dispatch import receiver from django.dispatch import receiver
from authentik.core.models import USER_PATH_SYSTEM_PREFIX, Token, TokenIntents, User, UserTypes from authentik.core.models import Token, TokenIntents, User, UserTypes
from authentik.sources.scim.models import SCIMSource from authentik.sources.scim.models import SCIMSource
USER_PATH_SOURCE_SCIM = USER_PATH_SYSTEM_PREFIX + "/sources/scim"
@receiver(pre_save, sender=SCIMSource) @receiver(pre_save, sender=SCIMSource)
def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_): def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
@ -18,7 +16,6 @@ def scim_source_pre_save(sender: type[Model], instance: SCIMSource, **_):
username=identifier, username=identifier,
name=f"SCIM Source {instance.name} Service-Account", name=f"SCIM Source {instance.name} Service-Account",
type=UserTypes.INTERNAL_SERVICE_ACCOUNT, type=UserTypes.INTERNAL_SERVICE_ACCOUNT,
path=USER_PATH_SOURCE_SCIM,
) )
token = Token.objects.create( token = Token.objects.create(
user=user, user=user,

View File

@ -13,7 +13,6 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from authentik.core.models import Group, User from authentik.core.models import Group, User
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import Group as SCIMGroupModel from authentik.providers.scim.clients.schema import Group as SCIMGroupModel
from authentik.sources.scim.models import SCIMSourceGroup from authentik.sources.scim.models import SCIMSourceGroup
from authentik.sources.scim.views.v2.base import SCIMView from authentik.sources.scim.views.v2.base import SCIMView
@ -27,11 +26,9 @@ class GroupsView(SCIMView):
def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict: def group_to_scim(self, scim_group: SCIMSourceGroup) -> dict:
"""Convert Group to SCIM data""" """Convert Group to SCIM data"""
payload = SCIMGroupModel( payload = SCIMGroupModel(
schemas=[SCIM_USER_SCHEMA],
id=str(scim_group.group.pk), id=str(scim_group.group.pk),
externalId=scim_group.id, externalId=scim_group.id,
displayName=scim_group.group.name, displayName=scim_group.group.name,
members=[],
meta={ meta={
"resourceType": "Group", "resourceType": "Group",
"location": self.request.build_absolute_uri( "location": self.request.build_absolute_uri(
@ -45,24 +42,28 @@ class GroupsView(SCIMView):
), ),
}, },
) )
for member in scim_group.group.users.order_by("pk"): return payload.model_dump(
member: User mode="json",
payload.members.append(GroupMember(value=str(member.uuid))) exclude_unset=True,
return payload.model_dump(mode="json", exclude_unset=True) )
def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response: def get(self, request: Request, group_id: str | None = None, **kwargs) -> Response:
"""List Group handler""" """List Group handler"""
base_query = SCIMSourceGroup.objects.select_related("group").prefetch_related(
"group__users"
)
if group_id: if group_id:
connection = base_query.filter(source=self.source, group__group_uuid=group_id).first() connection = (
SCIMSourceGroup.objects.filter(source=self.source, group__group_uuid=group_id)
.select_related("group")
.first()
)
if not connection: if not connection:
raise Http404 raise Http404
return Response(self.group_to_scim(connection)) return Response(self.group_to_scim(connection))
connections = ( connections = (
base_query.filter(source=self.source).order_by("pk").filter(self.filter_parse(request)) SCIMSourceGroup.objects.filter(source=self.source)
.select_related("group")
.order_by("pk")
) )
connections = connections.filter(self.filter_parse(request))
page = self.paginate_query(connections) page = self.paginate_query(connections)
return Response( return Response(
{ {
@ -78,8 +79,6 @@ class GroupsView(SCIMView):
def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict): def update_group(self, connection: SCIMSourceGroup | None, data: QueryDict):
"""Partial update a group""" """Partial update a group"""
group = connection.group if connection else Group() group = connection.group if connection else Group()
if _group := Group.objects.filter(name=data.get("displayName")).first():
group = _group
if "displayName" in data: if "displayName" in data:
group.name = data.get("displayName") group.name = data.get("displayName")
if group.name == "": if group.name == "":

View File

@ -11,7 +11,6 @@ from rest_framework.request import Request
from rest_framework.response import Response from rest_framework.response import Response
from authentik.core.models import User from authentik.core.models import User
from authentik.providers.scim.clients.schema import SCIM_USER_SCHEMA
from authentik.providers.scim.clients.schema import User as SCIMUserModel from authentik.providers.scim.clients.schema import User as SCIMUserModel
from authentik.sources.scim.models import SCIMSourceUser from authentik.sources.scim.models import SCIMSourceUser
from authentik.sources.scim.views.v2.base import SCIMView from authentik.sources.scim.views.v2.base import SCIMView
@ -34,7 +33,6 @@ class UsersView(SCIMView):
def user_to_scim(self, scim_user: SCIMSourceUser) -> dict: def user_to_scim(self, scim_user: SCIMSourceUser) -> dict:
"""Convert User to SCIM data""" """Convert User to SCIM data"""
payload = SCIMUserModel( payload = SCIMUserModel(
schemas=[SCIM_USER_SCHEMA],
id=str(scim_user.user.uuid), id=str(scim_user.user.uuid),
externalId=scim_user.id, externalId=scim_user.id,
userName=scim_user.user.username, userName=scim_user.user.username,
@ -64,7 +62,10 @@ class UsersView(SCIMView):
), ),
}, },
) )
final_payload = payload.model_dump(mode="json", exclude_unset=True) final_payload = payload.model_dump(
mode="json",
exclude_unset=True,
)
final_payload.update(scim_user.attributes) final_payload.update(scim_user.attributes)
return final_payload return final_payload
@ -98,8 +99,6 @@ class UsersView(SCIMView):
def update_user(self, connection: SCIMSourceUser | None, data: QueryDict): def update_user(self, connection: SCIMSourceUser | None, data: QueryDict):
"""Partial update a user""" """Partial update a user"""
user = connection.user if connection else User() user = connection.user if connection else User()
if _user := User.objects.filter(username=data.get("userName")).first():
user = _user
user.path = self.source.get_user_path() user.path = self.source.get_user_path()
if "userName" in data: if "userName" in data:
user.username = data.get("userName") user.username = data.get("userName")

View File

@ -96,7 +96,7 @@ class DuoDevice(SerializerModel, Device):
return DuoDeviceSerializer return DuoDeviceSerializer
def __str__(self): def __str__(self):
return str(self.name) or str(self.user_id) return str(self.name) or str(self.user)
class Meta: class Meta:
verbose_name = _("Duo Device") verbose_name = _("Duo Device")

View File

@ -221,7 +221,7 @@ class SMSDevice(SerializerModel, SideChannelDevice):
return valid return valid
def __str__(self): def __str__(self):
return str(self.name) or str(self.user_id) return str(self.name) or str(self.user)
class Meta: class Meta:
verbose_name = _("SMS Device") verbose_name = _("SMS Device")

View File

@ -155,7 +155,7 @@ class WebAuthnDevice(SerializerModel, Device):
return WebAuthnDeviceSerializer return WebAuthnDeviceSerializer
def __str__(self): def __str__(self):
return str(self.name) or str(self.user_id) return str(self.name) or str(self.user)
class Meta: class Meta:
verbose_name = _("WebAuthn Device") verbose_name = _("WebAuthn Device")

View File

@ -65,7 +65,7 @@ class UserConsent(SerializerModel, ExpiringModel):
return UserConsentSerializer return UserConsentSerializer
def __str__(self): def __str__(self):
return f"User Consent {self.application_id} by {self.user_id}" return f"User Consent {self.application} by {self.user}"
class Meta: class Meta:
unique_together = (("user", "application", "permissions"),) unique_together = (("user", "application", "permissions"),)

View File

@ -79,7 +79,7 @@ class Invitation(SerializerModel, ExpiringModel):
return InvitationSerializer return InvitationSerializer
def __str__(self): def __str__(self):
return f"Invitation {str(self.invite_uuid)} created by {self.created_by_id}" return f"Invitation {str(self.invite_uuid)} created by {self.created_by}"
class Meta: class Meta:
verbose_name = _("Invitation") verbose_name = _("Invitation")

View File

@ -1,23 +0,0 @@
# Generated by Django 5.0.4 on 2024-05-01 15:32
import authentik.lib.utils.time
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_tenants", "0002_tenant_default_token_duration_and_more"),
]
operations = [
migrations.AlterField(
model_name="tenant",
name="default_token_duration",
field=models.TextField(
default="days=1",
help_text="Default token duration",
validators=[authentik.lib.utils.time.timedelta_string_validator],
),
),
]

View File

@ -3,7 +3,6 @@
from tenant_schemas_celery.scheduler import ( from tenant_schemas_celery.scheduler import (
TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler, TenantAwarePersistentScheduler as BaseTenantAwarePersistentScheduler,
) )
from tenant_schemas_celery.scheduler import TenantAwareScheduleEntry
class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler): class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
@ -12,11 +11,3 @@ class TenantAwarePersistentScheduler(BaseTenantAwarePersistentScheduler):
@classmethod @classmethod
def get_queryset(cls): def get_queryset(cls):
return super().get_queryset().filter(ready=True) return super().get_queryset().filter(ready=True)
def apply_entry(self, entry: TenantAwareScheduleEntry, producer=None):
# https://github.com/maciej-gol/tenant-schemas-celery/blob/master/tenant_schemas_celery/scheduler.py#L85
# When (as by default) no tenant schemas are set, the public schema is excluded
# so we need to explicitly include it here, otherwise the task is not executed
if entry.tenant_schemas is None:
entry.tenant_schemas = self.get_queryset().values_list("schema_name", flat=True)
return super().apply_entry(entry, producer)

View File

@ -2,7 +2,7 @@
"$schema": "http://json-schema.org/draft-07/schema", "$schema": "http://json-schema.org/draft-07/schema",
"$id": "https://goauthentik.io/blueprints/schema.json", "$id": "https://goauthentik.io/blueprints/schema.json",
"type": "object", "type": "object",
"title": "authentik 2024.4.2 Blueprint schema", "title": "authentik 2024.4.0 Blueprint schema",
"required": [ "required": [
"version", "version",
"entries" "entries"
@ -4131,10 +4131,6 @@
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
"http://www.w3.org/2000/09/xmldsig#dsa-sha1" "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
], ],
"title": "Signature algorithm" "title": "Signature algorithm"
@ -4939,10 +4935,6 @@
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha384", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#rsa-sha512", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384",
"http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512",
"http://www.w3.org/2000/09/xmldsig#dsa-sha1" "http://www.w3.org/2000/09/xmldsig#dsa-sha1"
], ],
"title": "Signature algorithm" "title": "Signature algorithm"

View File

@ -32,7 +32,7 @@ services:
volumes: volumes:
- redis:/data - redis:/data
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.0}
restart: unless-stopped restart: unless-stopped
command: server command: server
environment: environment:
@ -53,7 +53,7 @@ services:
- postgresql - postgresql
- redis - redis
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.2} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2024.4.0}
restart: unless-stopped restart: unless-stopped
command: worker command: worker
environment: environment:

2
go.mod
View File

@ -28,7 +28,7 @@ require (
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/stretchr/testify v1.9.0 github.com/stretchr/testify v1.9.0
github.com/wwt/guac v1.3.2 github.com/wwt/guac v1.3.2
goauthentik.io/api/v3 v3.2024023.2 goauthentik.io/api/v3 v3.2024040.1
golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab golang.org/x/exp v0.0.0-20230210204819-062eb4c674ab
golang.org/x/oauth2 v0.19.0 golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0 golang.org/x/sync v0.7.0

4
go.sum
View File

@ -294,8 +294,8 @@ go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
goauthentik.io/api/v3 v3.2024023.2 h1:lSVaZAKTpsDhtw11wnkGjPalkDzv9H2VKEJllBi2aXs= goauthentik.io/api/v3 v3.2024040.1 h1:0Mp8XLYuscQEWVTR2lNk74WLKDpOVHX0mlbvbvcC6fw=
goauthentik.io/api/v3 v3.2024023.2/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw= goauthentik.io/api/v3 v3.2024040.1/go.mod h1:zz+mEZg8rY/7eEjkMGWJ2DnGqk+zqxuybGCGrR2O4Kw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=

View File

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

View File

@ -117,8 +117,6 @@ def run_migrations():
) )
finally: finally:
release_lock(curr) release_lock(curr)
curr.close()
conn.close()
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -3,6 +3,7 @@
import authentik. This is done by the dockerfile.""" import authentik. This is done by the dockerfile."""
from sys import exit as sysexit from sys import exit as sysexit
from time import sleep from time import sleep
from urllib.parse import quote_plus
from psycopg import OperationalError, connect from psycopg import OperationalError, connect
from redis import Redis from redis import Redis
@ -34,7 +35,7 @@ def check_postgres():
def check_redis(): def check_redis():
url = CONFIG.get("cache.url") or redis_url(CONFIG.get("redis.db")) url = redis_url(CONFIG.get("redis.db"))
while True: while True:
try: try:
redis = Redis.from_url(url) redis = Redis.from_url(url)
@ -42,7 +43,10 @@ def check_redis():
break break
except RedisError as exc: except RedisError as exc:
sleep(1) sleep(1)
CONFIG.log("info", f"Redis Connection failed, retrying... ({exc})") sanitized_url = url.replace(quote_plus(CONFIG.get("redis.password")), "******")
CONFIG.log(
"info", f"Redis Connection failed, retrying... ({exc})", redis_url=sanitized_url
)
CONFIG.log("info", "Redis Connection successful") CONFIG.log("info", "Redis Connection successful")

File diff suppressed because it is too large Load Diff

Binary file not shown.

2284
poetry.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "authentik" name = "authentik"
version = "2024.4.2" version = "2024.4.0"
description = "" description = ""
authors = ["authentik Team <hello@goauthentik.io>"] authors = ["authentik Team <hello@goauthentik.io>"]
@ -89,7 +89,6 @@ channels = { version = "*", extras = ["daphne"] }
channels-redis = "*" channels-redis = "*"
codespell = "*" codespell = "*"
colorama = "*" colorama = "*"
cryptography = "*"
dacite = "*" dacite = "*"
deepmerge = "*" deepmerge = "*"
defusedxml = "*" defusedxml = "*"
@ -102,7 +101,7 @@ django-redis = "*"
django-storages = { extras = ["s3"], version = "*" } django-storages = { extras = ["s3"], version = "*" }
# See https://github.com/django-tenants/django-tenants/pull/997 # See https://github.com/django-tenants/django-tenants/pull/997
django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch="authentik-fixes" } django-tenants = { git = "https://github.com/rissson/django-tenants.git", branch="authentik-fixes" }
djangorestframework = "3.14.0" djangorestframework = "*"
djangorestframework-guardian = "*" djangorestframework-guardian = "*"
docker = "*" docker = "*"
drf-spectacular = "*" drf-spectacular = "*"
@ -116,11 +115,17 @@ gunicorn = "*"
jsonpatch = "*" jsonpatch = "*"
kubernetes = "*" kubernetes = "*"
ldap3 = "*" ldap3 = "*"
lxml = "*" lxml = [
# 5.0.0 works with libxml2 2.11.x, which is standard on brew
{ version = "5.0.0", platform = "darwin" },
# 4.9.x works with previous libxml2 versions, which is what we get on linux
{ version = "4.9.4", platform = "linux" },
]
opencontainers = { extras = ["reggie"], version = "*" } opencontainers = { extras = ["reggie"], version = "*" }
packaging = "*" packaging = "*"
paramiko = "*" paramiko = "*"
psycopg = { extras = ["c"], version = "*" } psycopg = { extras = ["c"], version = "*" }
pycryptodome = "*"
pydantic = "*" pydantic = "*"
pydantic-scim = "*" pydantic-scim = "*"
pyjwt = "*" pyjwt = "*"

View File

@ -1,7 +1,7 @@
openapi: 3.0.3 openapi: 3.0.3
info: info:
title: authentik title: authentik
version: 2024.4.2 version: 2024.4.0
description: Making authentication simple. description: Making authentication simple.
contact: contact:
email: hello@goauthentik.io email: hello@goauthentik.io
@ -17051,10 +17051,6 @@ paths:
enum: enum:
- http://www.w3.org/2000/09/xmldsig#dsa-sha1 - http://www.w3.org/2000/09/xmldsig#dsa-sha1
- http://www.w3.org/2000/09/xmldsig#rsa-sha1 - http://www.w3.org/2000/09/xmldsig#rsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
@ -20914,10 +20910,6 @@ paths:
enum: enum:
- http://www.w3.org/2000/09/xmldsig#dsa-sha1 - http://www.w3.org/2000/09/xmldsig#dsa-sha1
- http://www.w3.org/2000/09/xmldsig#rsa-sha1 - http://www.w3.org/2000/09/xmldsig#rsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
@ -30458,11 +30450,6 @@ components:
- pending_user - pending_user
- pending_user_avatar - pending_user_avatar
- type - type
AlgEnum:
enum:
- rsa
- ecdsa
type: string
App: App:
type: object type: object
description: Serialize Application info description: Serialize Application info
@ -32120,10 +32107,6 @@ components:
type: string type: string
validity_days: validity_days:
type: integer type: integer
alg:
allOf:
- $ref: '#/components/schemas/AlgEnum'
default: rsa
required: required:
- common_name - common_name
- validity_days - validity_days
@ -43675,10 +43658,6 @@ components:
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha384 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#rsa-sha512 - http://www.w3.org/2001/04/xmldsig-more#rsa-sha512
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha1
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha256
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha384
- http://www.w3.org/2001/04/xmldsig-more#ecdsa-sha512
- http://www.w3.org/2000/09/xmldsig#dsa-sha1 - http://www.w3.org/2000/09/xmldsig#dsa-sha1
type: string type: string
Source: Source:

View File

@ -12,10 +12,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0", "@typescript-eslint/parser": "^7.5.0",
"@wdio/cli": "^8.36.0", "@wdio/cli": "^8.36.1",
"@wdio/local-runner": "^8.36.0", "@wdio/local-runner": "^8.36.1",
"@wdio/mocha-framework": "^8.36.0", "@wdio/mocha-framework": "^8.36.1",
"@wdio/spec-reporter": "^8.36.0", "@wdio/spec-reporter": "^8.36.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.1", "eslint-plugin-sonarjs": "^0.25.1",
@ -1189,19 +1189,19 @@
} }
}, },
"node_modules/@wdio/cli": { "node_modules/@wdio/cli": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/cli/-/cli-8.36.1.tgz",
"integrity": "sha512-B8iEwz9DRzHquPihT74nKUzN9s+rCd1TkBp+JGmdgm7pJqiWTe4FORrzaxWjdiCO78jbYK9LgaMORpCcAzjwIA==", "integrity": "sha512-LZBZiwcvvv5P0HuRXt8IV09UiFT5dnDr1Ag5u2roJL2D7l8wDHHa70PXw9MmlbrnyFCUN3hO7FQVUi9MAsDbDQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.1.1", "@types/node": "^20.1.1",
"@vitest/snapshot": "^1.2.1", "@vitest/snapshot": "^1.2.1",
"@wdio/config": "8.36.0", "@wdio/config": "8.36.1",
"@wdio/globals": "8.36.0", "@wdio/globals": "8.36.1",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0", "@wdio/protocols": "8.32.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"@wdio/utils": "8.36.0", "@wdio/utils": "8.36.1",
"async-exit-hook": "^2.0.1", "async-exit-hook": "^2.0.1",
"chalk": "^5.2.0", "chalk": "^5.2.0",
"chokidar": "^3.5.3", "chokidar": "^3.5.3",
@ -1216,7 +1216,7 @@
"lodash.union": "^4.6.0", "lodash.union": "^4.6.0",
"read-pkg-up": "10.0.0", "read-pkg-up": "10.0.0",
"recursive-readdir": "^2.2.3", "recursive-readdir": "^2.2.3",
"webdriverio": "8.36.0", "webdriverio": "8.36.1",
"yargs": "^17.7.2" "yargs": "^17.7.2"
}, },
"bin": { "bin": {
@ -1239,14 +1239,14 @@
} }
}, },
"node_modules/@wdio/config": { "node_modules/@wdio/config": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/config/-/config-8.36.1.tgz",
"integrity": "sha512-sAbqnx/G+OsrMquIncFXjM4U0/E0ULMP0jDHZND75r0e1DYYCHmyacrvIHu3Jyxinl9f6+4XQdev6vqdTqPdNg==", "integrity": "sha512-yCENnym0CrYuLKMJ3fv00WkjCR8QpPqVohGBkq5FvZOZpVJEpoG86Q8l4HtyRnd6ggMTKCA1vTQ/myhbPmZmaQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"@wdio/utils": "8.36.0", "@wdio/utils": "8.36.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"deepmerge-ts": "^5.0.0", "deepmerge-ts": "^5.0.0",
"glob": "^10.2.2", "glob": "^10.2.2",
@ -1257,29 +1257,29 @@
} }
}, },
"node_modules/@wdio/globals": { "node_modules/@wdio/globals": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/globals/-/globals-8.36.1.tgz",
"integrity": "sha512-vqMq1hR+iF0lqMNJpk9z+QB9l/QfL1DbvOfNhPtQ13NgctfNg42ffuhEObbzTLQN0MftcnPBu6O3pai79y8bUA==", "integrity": "sha512-Qpj6gZCRNxqdVkTwYyi4JdeYO4tLSUj3Ti6yxO0v9A4IRaKW1tS29KUcGgjL9CFSBKAOi2zRY8vvFz1u6ewxtQ==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": "^16.13 || >=18" "node": "^16.13 || >=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"expect-webdriverio": "^4.11.2", "expect-webdriverio": "^4.11.2",
"webdriverio": "8.36.0" "webdriverio": "8.36.1"
} }
}, },
"node_modules/@wdio/local-runner": { "node_modules/@wdio/local-runner": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/local-runner/-/local-runner-8.36.1.tgz",
"integrity": "sha512-MIzbWcXgRQGQQK4H5N39/JFoikOg5cu34l1U6rgw74D6hO79L4RwBg2Oo4TJJYgHUL/4RbVwyeLdb5WDTdluTQ==", "integrity": "sha512-FYsTzbNGRnrniOsLWrZO7+DLecAS9W75AIzFZQVQxruiDFkGmKY5OV6gsuvMlasaqAQXW1s+w29bqrLY4DxdEw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/repl": "8.24.12", "@wdio/repl": "8.24.12",
"@wdio/runner": "8.36.0", "@wdio/runner": "8.36.1",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"async-exit-hook": "^2.0.1", "async-exit-hook": "^2.0.1",
"split2": "^4.1.0", "split2": "^4.1.0",
"stream-buffers": "^3.0.2" "stream-buffers": "^3.0.2"
@ -1316,16 +1316,16 @@
} }
}, },
"node_modules/@wdio/mocha-framework": { "node_modules/@wdio/mocha-framework": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/mocha-framework/-/mocha-framework-8.36.1.tgz",
"integrity": "sha512-5wZgh1apbSKTtgGwvd//L4kxdaXe30AQ3y9YeJD+OuAJUTYFRjTpMS13bO3pX518imQeB8HCm4aUc2kxs7J81Q==", "integrity": "sha512-G0h5AeneMNtoh9CcVQ82OCKj0axxUOEotEcInDu8V6UJbUywNJVL/bdTMKdaq5i84Hnc+s1LUKmLvN95F+lHGA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/mocha": "^10.0.0", "@types/mocha": "^10.0.0",
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"@wdio/utils": "8.36.0", "@wdio/utils": "8.36.1",
"mocha": "^10.0.0" "mocha": "^10.0.0"
}, },
"engines": { "engines": {
@ -1351,14 +1351,14 @@
} }
}, },
"node_modules/@wdio/reporter": { "node_modules/@wdio/reporter": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/reporter/-/reporter-8.36.1.tgz",
"integrity": "sha512-pkAxqiMC+ljmksOKlK9g6y2NRvrdQiKtxD11rsMwJ6CH4kVDSGIvENw7u3kxg7Qwp0j1rCKf5Hp51npqKQgeDQ==", "integrity": "sha512-HcXr9XKq/6kPC9nexMRXIc/ft3Lvp0yCaW5tps01Axus9wbi5ysLHi2z5sB84F2YdpM+aRf7Lac56xkc4Jldeg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"diff": "^5.0.0", "diff": "^5.0.0",
"object-inspect": "^1.12.0" "object-inspect": "^1.12.0"
}, },
@ -1367,35 +1367,35 @@
} }
}, },
"node_modules/@wdio/runner": { "node_modules/@wdio/runner": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/runner/-/runner-8.36.1.tgz",
"integrity": "sha512-M2ZDL0gmR2VvVMchi3Pkonva6Gn6eFh6IwVCpT0np7zioaqOksy3IM7Aki8kPKKS88Osip5dAfoKIrY7JpHovA==", "integrity": "sha512-bLkxQ46MLEbzIf30adl2nyz8kxED/V0IjcQASm0VKfNmsG8LOf7iOIz+udOF4GkMoF++5JuONA5abUsyLvwatg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.11.28", "@types/node": "^20.11.28",
"@wdio/config": "8.36.0", "@wdio/config": "8.36.1",
"@wdio/globals": "8.36.0", "@wdio/globals": "8.36.1",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"@wdio/utils": "8.36.0", "@wdio/utils": "8.36.1",
"deepmerge-ts": "^5.1.0", "deepmerge-ts": "^5.1.0",
"expect-webdriverio": "^4.12.0", "expect-webdriverio": "^4.12.0",
"gaze": "^1.1.3", "gaze": "^1.1.3",
"webdriver": "8.36.0", "webdriver": "8.36.1",
"webdriverio": "8.36.0" "webdriverio": "8.36.1"
}, },
"engines": { "engines": {
"node": "^16.13 || >=18" "node": "^16.13 || >=18"
} }
}, },
"node_modules/@wdio/spec-reporter": { "node_modules/@wdio/spec-reporter": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/spec-reporter/-/spec-reporter-8.36.1.tgz",
"integrity": "sha512-GVOiWqVYvzoAo4/4hNVxvyVWVoHyEmAywYhkykyJGL05YpO0oDOZY2kINPePEX5Z+nIsXsiKPmtsGGqWsfQwTw==", "integrity": "sha512-VgAd8VQCfwKYz4A3BPDUYNIQxXhRSTaVNbmDzSlYfo5Jekygk7fz0LRFYBpJ69l7eQH0P5nzEyF92oW/rvE3VA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@wdio/reporter": "8.36.0", "@wdio/reporter": "8.36.1",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"chalk": "^5.1.2", "chalk": "^5.1.2",
"easy-table": "^1.2.0", "easy-table": "^1.2.0",
"pretty-ms": "^7.0.0" "pretty-ms": "^7.0.0"
@ -1417,9 +1417,9 @@
} }
}, },
"node_modules/@wdio/types": { "node_modules/@wdio/types": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/types/-/types-8.36.1.tgz",
"integrity": "sha512-0hw/PaJHqDrbIMvU08w3oMDGg89udSkqWF2hFlGAjOc20quRrhn0F1L+NhFpYdezeRKz5gpgTDIqaQs9RWKq1A==", "integrity": "sha512-kKtyJbypasKo/VQuJ6dTQQwFtHE9qoygjoCZjrQCLGraRSjOEiqZHPR0497wbeCvcgHIYyImbmcylqZNGUE0CQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.1.0" "@types/node": "^20.1.0"
@ -1429,14 +1429,14 @@
} }
}, },
"node_modules/@wdio/utils": { "node_modules/@wdio/utils": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.0.tgz", "resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-8.36.1.tgz",
"integrity": "sha512-3VAbavN206qkvm6lITtOtTgscFChax7shzqHjUNln+QWMRyELtT81iw32ux2ld+Bg3F60LAmhbGodu0lJH7k2w==", "integrity": "sha512-xmgPHU11/o9n2FeRmDFkPRC0okiwA1i2xOcR2c3aSpuk99XkAm9RaMn/6u9LFaqsCpgaVxazcYEGSceO7U4hZA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@puppeteer/browsers": "^1.6.0", "@puppeteer/browsers": "^1.6.0",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"decamelize": "^6.0.0", "decamelize": "^6.0.0",
"deepmerge-ts": "^5.1.0", "deepmerge-ts": "^5.1.0",
"edgedriver": "^5.3.5", "edgedriver": "^5.3.5",
@ -8886,18 +8886,18 @@
} }
}, },
"node_modules/webdriver": { "node_modules/webdriver": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.0.tgz", "resolved": "https://registry.npmjs.org/webdriver/-/webdriver-8.36.1.tgz",
"integrity": "sha512-6fmZI1+OCGbhuGMLBLvA7m9TJvHU1Cyzxqd8rGzIyb8hocR53yh/olfOL1BPcjU1NXmKuU1BePSGF+yiKajiEA==", "integrity": "sha512-547RivYCHStVqtiGQBBcABAkzJbPnAWsxpXGzmj5KL+TOM2JF41N2iQRtUxXqr0jme1Nzzye7WS7Y7iSnK6i1g==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@types/ws": "^8.5.3", "@types/ws": "^8.5.3",
"@wdio/config": "8.36.0", "@wdio/config": "8.36.1",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0", "@wdio/protocols": "8.32.0",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"@wdio/utils": "8.36.0", "@wdio/utils": "8.36.1",
"deepmerge-ts": "^5.1.0", "deepmerge-ts": "^5.1.0",
"got": "^12.6.1", "got": "^12.6.1",
"ky": "^0.33.0", "ky": "^0.33.0",
@ -8908,18 +8908,18 @@
} }
}, },
"node_modules/webdriverio": { "node_modules/webdriverio": {
"version": "8.36.0", "version": "8.36.1",
"resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.0.tgz", "resolved": "https://registry.npmjs.org/webdriverio/-/webdriverio-8.36.1.tgz",
"integrity": "sha512-4WnEI+OxslHpfSnDXuADaR6bL1M7QxBUEF1mTN56AroOCJelyPvt94yRhszwQnLcJJB2OLn49eUz8M4yBCB51w==", "integrity": "sha512-vzE09oFQeMbOYJ/75jZ13sDIljzC3HH7uoUJKAMAEtyrn/bu1F9Sg/4IDEsvQaRD3pz3ae6SkRld33lcQk6HJA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "^20.1.0", "@types/node": "^20.1.0",
"@wdio/config": "8.36.0", "@wdio/config": "8.36.1",
"@wdio/logger": "8.28.0", "@wdio/logger": "8.28.0",
"@wdio/protocols": "8.32.0", "@wdio/protocols": "8.32.0",
"@wdio/repl": "8.24.12", "@wdio/repl": "8.24.12",
"@wdio/types": "8.36.0", "@wdio/types": "8.36.1",
"@wdio/utils": "8.36.0", "@wdio/utils": "8.36.1",
"archiver": "^7.0.0", "archiver": "^7.0.0",
"aria-query": "^5.0.0", "aria-query": "^5.0.0",
"css-shorthand-properties": "^1.1.1", "css-shorthand-properties": "^1.1.1",
@ -8936,7 +8936,7 @@
"resq": "^1.9.1", "resq": "^1.9.1",
"rgb2hex": "0.2.5", "rgb2hex": "0.2.5",
"serialize-error": "^11.0.1", "serialize-error": "^11.0.1",
"webdriver": "8.36.0" "webdriver": "8.36.1"
}, },
"engines": { "engines": {
"node": "^16.13 || >=18" "node": "^16.13 || >=18"

View File

@ -6,10 +6,10 @@
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/eslint-plugin": "^7.5.0",
"@typescript-eslint/parser": "^7.5.0", "@typescript-eslint/parser": "^7.5.0",
"@wdio/cli": "^8.36.0", "@wdio/cli": "^8.36.1",
"@wdio/local-runner": "^8.36.0", "@wdio/local-runner": "^8.36.1",
"@wdio/mocha-framework": "^8.36.0", "@wdio/mocha-framework": "^8.36.1",
"@wdio/spec-reporter": "^8.36.0", "@wdio/spec-reporter": "^8.36.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-google": "^0.14.0", "eslint-config-google": "^0.14.0",
"eslint-plugin-sonarjs": "^0.25.1", "eslint-plugin-sonarjs": "^0.25.1",

2402
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -38,7 +38,7 @@
"@codemirror/theme-one-dark": "^6.1.2", "@codemirror/theme-one-dark": "^6.1.2",
"@formatjs/intl-listformat": "^7.5.5", "@formatjs/intl-listformat": "^7.5.5",
"@fortawesome/fontawesome-free": "^6.5.2", "@fortawesome/fontawesome-free": "^6.5.2",
"@goauthentik/api": "^2024.4.1-1714655911", "@goauthentik/api": "^2024.4.0-1713978791",
"@lit-labs/task": "^3.1.0", "@lit-labs/task": "^3.1.0",
"@lit/context": "^1.1.1", "@lit/context": "^1.1.1",
"@lit/localize": "^0.12.1", "@lit/localize": "^0.12.1",
@ -46,7 +46,7 @@
"@open-wc/lit-helpers": "^0.7.0", "@open-wc/lit-helpers": "^0.7.0",
"@patternfly/elements": "^3.0.1", "@patternfly/elements": "^3.0.1",
"@patternfly/patternfly": "^4.224.2", "@patternfly/patternfly": "^4.224.2",
"@sentry/browser": "^7.111.0", "@sentry/browser": "^7.112.2",
"@webcomponents/webcomponentsjs": "^2.8.0", "@webcomponents/webcomponentsjs": "^2.8.0",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"chart.js": "^4.4.2", "chart.js": "^4.4.2",
@ -81,13 +81,13 @@
"@lit/localize-tools": "^0.7.2", "@lit/localize-tools": "^0.7.2",
"@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-replace": "^5.0.5",
"@spotlightjs/spotlight": "^1.2.17", "@spotlightjs/spotlight": "^1.2.17",
"@storybook/addon-essentials": "^8.0.8", "@storybook/addon-essentials": "^8.0.9",
"@storybook/addon-links": "^8.0.8", "@storybook/addon-links": "^8.0.9",
"@storybook/api": "^7.6.17", "@storybook/api": "^7.6.17",
"@storybook/blocks": "^8.0.8", "@storybook/blocks": "^8.0.8",
"@storybook/manager-api": "^8.0.8", "@storybook/manager-api": "^8.0.9",
"@storybook/web-components": "^8.0.8", "@storybook/web-components": "^8.0.9",
"@storybook/web-components-vite": "^8.0.8", "@storybook/web-components-vite": "^8.0.9",
"@trivago/prettier-plugin-sort-imports": "^4.3.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0",
"@types/chart.js": "^2.9.41", "@types/chart.js": "^2.9.41",
"@types/codemirror": "5.60.15", "@types/codemirror": "5.60.15",
@ -117,7 +117,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"rollup-plugin-modify": "^3.0.0", "rollup-plugin-modify": "^3.0.0",
"rollup-plugin-postcss-lit": "^2.1.0", "rollup-plugin-postcss-lit": "^2.1.0",
"storybook": "^8.0.8", "storybook": "^8.0.9",
"storybook-addon-mock": "^5.0.0", "storybook-addon-mock": "^5.0.0",
"ts-lit-plugin": "^2.0.2", "ts-lit-plugin": "^2.0.2",
"tslib": "^2.6.2", "tslib": "^2.6.2",
@ -129,9 +129,9 @@
"@esbuild/darwin-arm64": "^0.20.1", "@esbuild/darwin-arm64": "^0.20.1",
"@esbuild/linux-amd64": "^0.18.11", "@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.20.1", "@esbuild/linux-arm64": "^0.20.1",
"@rollup/rollup-darwin-arm64": "4.14.3", "@rollup/rollup-darwin-arm64": "4.16.4",
"@rollup/rollup-linux-arm64-gnu": "4.14.3", "@rollup/rollup-linux-arm64-gnu": "4.16.4",
"@rollup/rollup-linux-x64-gnu": "4.14.3" "@rollup/rollup-linux-x64-gnu": "4.16.4"
}, },
"engines": { "engines": {
"node": ">=20" "node": ">=20"

View File

@ -29,9 +29,5 @@ export const signatureAlgorithmOptions = toOptions([
["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true], ["RSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMorersaSha256, true],
["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384], ["RSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMorersaSha384],
["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512], ["RSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMorersaSha512],
["ECDSA-SHA1", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha1],
["ECDSA-SHA256", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha256],
["ECDSA-SHA384", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha384],
["ECDSA-SHA512", SignatureAlgorithmEnum._200104XmldsigMoreecdsaSha512],
["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1], ["DSA-SHA1", SignatureAlgorithmEnum._200009XmldsigdsaSha1],
]); ]);

View File

@ -6,12 +6,7 @@ import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit"; import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js"; import { customElement } from "lit/decorators.js";
import { import { CertificateGenerationRequest, CertificateKeyPair, CryptoApi } from "@goauthentik/api";
AlgEnum,
CertificateGenerationRequest,
CertificateKeyPair,
CryptoApi,
} from "@goauthentik/api";
@customElement("ak-crypto-certificate-generate-form") @customElement("ak-crypto-certificate-generate-form")
export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> { export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
@ -45,29 +40,6 @@ export class CertificateKeyPairForm extends Form<CertificateGenerationRequest> {
?required=${true} ?required=${true}
> >
<input class="pf-c-form-control" type="number" value="365" /> <input class="pf-c-form-control" type="number" value="365" />
</ak-form-element-horizontal> </ak-form-element-horizontal>`;
<ak-form-element-horizontal
label=${msg("Private key Algorithm")}
?required=${true}
name="alg"
>
<ak-radio
.options=${[
{
label: msg("RSA"),
value: AlgEnum.Rsa,
default: true,
},
{
label: msg("ECDSA"),
value: AlgEnum.Ecdsa,
},
]}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Algorithm used to generate the private key.")}
</p>
</ak-form-element-horizontal> `;
} }
} }

View File

@ -97,7 +97,7 @@ export class EventListPage extends TablePage<Event> {
} }
renderExpanded(item: Event): TemplateResult { renderExpanded(item: Event): TemplateResult {
return html` <td role="cell" colspan="5"> return html` <td role="cell" colspan="3">
<div class="pf-c-table__expandable-row-content"> <div class="pf-c-table__expandable-row-content">
<ak-event-info .event=${item as EventWithContext}></ak-event-info> <ak-event-info .event=${item as EventWithContext}></ak-event-info>
</div> </div>

View File

@ -470,7 +470,7 @@ export class LDAPSourceForm extends BaseSourceForm<LDAPSource> {
> >
<input <input
type="text" type="text"
value="${this.instance?.objectUniquenessField || "objectSid"}" value="${this.instance?.objectUniquenessField || "entryDN"}"
class="pf-c-form-control" class="pf-c-form-control"
required required
/> />

View File

@ -128,14 +128,6 @@ export class UserForm extends ModelForm<User, number> {
"Service accounts should be used for machine-to-machine authentication or other automations.", "Service accounts should be used for machine-to-machine authentication or other automations.",
)}`, )}`,
}, },
{
label: "Internal Service account",
value: UserTypeEnum.InternalServiceAccount,
disabled: true,
description: html`${msg(
"Internal Service accounts are created and managed by authentik and cannot be created manually.",
)}`,
},
]} ]}
.value=${this.instance?.type} .value=${this.instance?.type}
> >

View File

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

View File

@ -187,9 +187,6 @@ input[type="date"]::-webkit-calendar-picker-indicator {
.pf-c-select__menu-item.pf-m-focus { .pf-c-select__menu-item.pf-m-focus {
--pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish); --pf-c-select__menu-item--focus--BackgroundColor: var(--ak-dark-background-light-ish);
} }
.pf-c-button:disabled {
color: var(--ak-dark-background-lighter);
}
.pf-c-button.pf-m-plain:hover { .pf-c-button.pf-m-plain:hover {
color: var(--ak-dark-foreground); color: var(--ak-dark-foreground);
} }

View File

@ -18,7 +18,7 @@ export function me(): Promise<SessionUser> {
if (!user.user.settings || !("locale" in user.user.settings)) { if (!user.user.settings || !("locale" in user.user.settings)) {
return user; return user;
} }
const locale: string | undefined = user.user.settings.locale; const locale = user.user.settings.locale;
if (locale && locale !== "") { if (locale && locale !== "") {
console.debug( console.debug(
`authentik/locale: Activating user's configured locale '${locale}'`, `authentik/locale: Activating user's configured locale '${locale}'`,

View File

@ -18,7 +18,6 @@ import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList
import PFList from "@patternfly/patternfly/components/List/list.css"; import PFList from "@patternfly/patternfly/components/List/list.css";
import PFTable from "@patternfly/patternfly/components/Table/table.css"; import PFTable from "@patternfly/patternfly/components/Table/table.css";
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css"; import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
import PFSplit from "@patternfly/patternfly/layouts/Split/split.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css"; import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { EventActions, FlowsApi } from "@goauthentik/api"; import { EventActions, FlowsApi } from "@goauthentik/api";
@ -82,7 +81,6 @@ export class EventInfo extends AKElement {
PFCard, PFCard,
PFTable, PFTable,
PFList, PFList,
PFSplit,
PFDescriptionList, PFDescriptionList,
css` css`
code { code {
@ -248,17 +246,11 @@ export class EventInfo extends AKElement {
renderModelChanged() { renderModelChanged() {
const diff = this.event.context.diff as unknown as { const diff = this.event.context.diff as unknown as {
[key: string]: { [key: string]: { new_value: unknown; previous_value: unknown };
new_value: unknown;
previous_value: unknown;
add?: unknown[];
remove?: unknown[];
clear?: boolean;
};
}; };
let diffBody = html``; let diffBody = html``;
if (diff) { if (diff) {
diffBody = html`<div class="pf-l-split__item pf-m-fill"> diffBody = html`<div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Changes made:")}</div> <div class="pf-c-card__title">${msg("Changes made:")}</div>
<table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid"> <table class="pf-c-table pf-m-compact pf-m-grid-md" role="grid">
<thead> <thead>
@ -270,36 +262,16 @@ export class EventInfo extends AKElement {
</thead> </thead>
<tbody role="rowgroup"> <tbody role="rowgroup">
${Object.keys(diff).map((key) => { ${Object.keys(diff).map((key) => {
const value = diff[key];
const previousCol = value.previous_value
? JSON.stringify(value.previous_value, null, 4)
: msg("-");
let newCol = html``;
if (value.add || value.remove) {
newCol = html`<ul class="pf-c-list">
${(value.add || value.remove)?.map((item) => {
let itemLabel = "";
if (value.add) {
itemLabel = msg(str`Added ID ${item}`);
} else if (value.remove) {
itemLabel = msg(str`Removed ID ${item}`);
}
return html`<li>${itemLabel}</li>`;
})}
</ul>`;
} else if (value.clear) {
newCol = html`${msg("Cleared")}`;
} else {
newCol = html`<pre>
${JSON.stringify(value.new_value, null, 4)}</pre
>`;
}
return html` <tr role="row"> return html` <tr role="row">
<td role="cell"><pre>${key}</pre></td> <td role="cell"><pre>${key}</pre></td>
<td role="cell"> <td role="cell">
<pre>${previousCol}</pre> <pre>
${JSON.stringify(diff[key].previous_value, null, 4)}</pre
>
</td>
<td role="cell">
<pre>${JSON.stringify(diff[key].new_value, null, 4)}</pre>
</td> </td>
<td role="cell">${newCol}</td>
</tr>`; </tr>`;
})} })}
</tbody> </tbody>
@ -308,8 +280,8 @@ ${JSON.stringify(value.new_value, null, 4)}</pre
</div>`; </div>`;
} }
return html` return html`
<div class="pf-l-split"> <div class="pf-l-flex">
<div class="pf-l-split__item pf-m-fill"> <div class="pf-l-flex__item">
<div class="pf-c-card__title">${msg("Affected model:")}</div> <div class="pf-c-card__title">${msg("Affected model:")}</div>
<div class="pf-c-card__body"> <div class="pf-c-card__body">
${this.getModelInfo(this.event.context?.model as EventModel)} ${this.getModelInfo(this.event.context?.model as EventModel)}

View File

@ -1,23 +1,22 @@
import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants"; import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController } from "lit";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
import { CoreApi } from "@goauthentik/api"; import { CoreApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface"; import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class BrandContextController implements ReactiveController { export class BrandContextController implements ReactiveController {
host!: ReactiveElementHost; host!: ReactiveElementHost<AkInterface>;
context!: ContextProvider<{ __context__: CurrentBrand | undefined }>; context!: ContextProvider<{ __context__: CurrentBrand | undefined }>;
constructor(host: ReactiveElementHost) { constructor(host: ReactiveElementHost<AkInterface>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikBrandContext, context: authentikBrandContext,

View File

@ -2,23 +2,22 @@ import { EVENT_REFRESH } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { globalAK } from "@goauthentik/common/global"; import { globalAK } from "@goauthentik/common/global";
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController } from "lit";
import type { Config } from "@goauthentik/api"; import type { Config } from "@goauthentik/api";
import { RootApi } from "@goauthentik/api"; import { RootApi } from "@goauthentik/api";
import type { AkInterface } from "./Interface"; import type { AkInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkInterface;
export class ConfigContextController implements ReactiveController { export class ConfigContextController implements ReactiveController {
host!: ReactiveElementHost; host!: ReactiveElementHost<AkInterface>;
context!: ContextProvider<{ __context__: Config | undefined }>; context!: ContextProvider<{ __context__: Config | undefined }>;
constructor(host: ReactiveElementHost) { constructor(host: ReactiveElementHost<AkInterface>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikConfigContext, context: authentikConfigContext,

View File

@ -1,23 +1,22 @@
import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants"; import { EVENT_REFRESH_ENTERPRISE } from "@goauthentik/authentik/common/constants";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config"; import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import type { ReactiveElementHost } from "@goauthentik/elements/types.js";
import { ContextProvider } from "@lit/context"; import { ContextProvider } from "@lit/context";
import { ReactiveController, ReactiveControllerHost } from "lit"; import type { ReactiveController } from "lit";
import type { LicenseSummary } from "@goauthentik/api"; import type { LicenseSummary } from "@goauthentik/api";
import { EnterpriseApi } from "@goauthentik/api"; import { EnterpriseApi } from "@goauthentik/api";
import type { AkEnterpriseInterface } from "./Interface"; import type { AkEnterpriseInterface } from "./Interface";
type ReactiveElementHost = Partial<ReactiveControllerHost> & AkEnterpriseInterface;
export class EnterpriseContextController implements ReactiveController { export class EnterpriseContextController implements ReactiveController {
host!: ReactiveElementHost; host!: ReactiveElementHost<AkEnterpriseInterface>;
context!: ContextProvider<{ __context__: LicenseSummary | undefined }>; context!: ContextProvider<{ __context__: LicenseSummary | undefined }>;
constructor(host: ReactiveElementHost) { constructor(host: ReactiveElementHost<AkEnterpriseInterface>) {
this.host = host; this.host = host;
this.context = new ContextProvider(this.host, { this.context = new ContextProvider(this.host, {
context: authentikEnterpriseContext, context: authentikEnterpriseContext,

View File

@ -1,13 +1,11 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { Constructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { Config } from "@goauthentik/api"; import type { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = new (...args: any[]) => T;
export function WithAuthentikConfig<T extends Constructor<LitElement>>( export function WithAuthentikConfig<T extends Constructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,

View File

@ -1,14 +1,12 @@
import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikBrandContext } from "@goauthentik/elements/AuthentikContexts";
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { CurrentBrand } from "@goauthentik/api"; import type { CurrentBrand } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any export function WithBrandConfig<T extends AbstractConstructor<LitElement>>(
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithBrandConfig<T extends Constructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,
) { ) {

View File

@ -1,4 +1,5 @@
import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikConfigContext } from "@goauthentik/elements/AuthentikContexts";
import type { AbstractConstructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
@ -6,9 +7,6 @@ import type { LitElement } from "lit";
import { CapabilitiesEnum } from "@goauthentik/api"; import { CapabilitiesEnum } from "@goauthentik/api";
import { Config } from "@goauthentik/api"; import { Config } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
// Using a unique, lexically scoped, and locally static symbol as the field name for the context // Using a unique, lexically scoped, and locally static symbol as the field name for the context
// means that it's inaccessible to any child class looking for it. It's one of the strongest privacy // means that it's inaccessible to any child class looking for it. It's one of the strongest privacy
// guarantees in JavaScript. // guarantees in JavaScript.
@ -45,7 +43,7 @@ class WCC {
* *
*/ */
export function WithCapabilitiesConfig<T extends Constructor<LitElement>>( export function WithCapabilitiesConfig<T extends AbstractConstructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,
) { ) {

View File

@ -1,13 +1,11 @@
import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts"; import { authentikEnterpriseContext } from "@goauthentik/elements/AuthentikContexts";
import { Constructor } from "@goauthentik/elements/types.js";
import { consume } from "@lit/context"; import { consume } from "@lit/context";
import type { LitElement } from "lit"; import type { LitElement } from "lit";
import type { LicenseSummary } from "@goauthentik/api"; import type { LicenseSummary } from "@goauthentik/api";
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Constructor<T = object> = abstract new (...args: any[]) => T;
export function WithLicenseSummary<T extends Constructor<LitElement>>( export function WithLicenseSummary<T extends Constructor<LitElement>>(
superclass: T, superclass: T,
subscribe = true, subscribe = true,

View File

@ -2,7 +2,7 @@ import { EVENT_LOCALE_CHANGE, EVENT_LOCALE_REQUEST } from "@goauthentik/common/c
import { customEvent } from "@goauthentik/elements/utils/customEvents"; import { customEvent } from "@goauthentik/elements/utils/customEvents";
import { LitElement, html } from "lit"; import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators.js"; import { customElement, property, state } from "lit/decorators.js";
import { WithBrandConfig } from "../Interface/brandProvider"; import { WithBrandConfig } from "../Interface/brandProvider";
import { initializeLocalization } from "./configureLocale"; import { initializeLocalization } from "./configureLocale";
@ -38,6 +38,9 @@ export class LocaleContext extends LocaleContextBase {
setLocale: LocaleSetter; setLocale: LocaleSetter;
@state()
userLocale = "";
constructor(code = DEFAULT_LOCALE) { constructor(code = DEFAULT_LOCALE) {
super(); super();
this.notifyApplication = this.notifyApplication.bind(this); this.notifyApplication = this.notifyApplication.bind(this);
@ -56,22 +59,30 @@ export class LocaleContext extends LocaleContextBase {
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
// Commenting out until we can come up with a better way of separating the
// "request user identity" with the session expiration heartbeat.
/*
new CoreApi(DEFAULT_CONFIG)
.coreUsersMeRetrieve()
.then((user) => (this.userLocale = user?.user?.settings?.locale ?? ""))
.catch(() => {});
*/
this.updateLocale(); this.updateLocale();
window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); window.addEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
} }
disconnectedCallback() { disconnectedCallback() {
window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler as EventListener); window.removeEventListener(EVENT_LOCALE_REQUEST, this.updateLocaleHandler);
super.disconnectedCallback(); super.disconnectedCallback();
} }
updateLocaleHandler(ev: CustomEvent<{ locale: string }>) { updateLocaleHandler(_ev: Event) {
console.debug("authentik/locale: Locale update request received."); console.debug("authentik/locale: Locale update request received.");
this.updateLocale(ev.detail.locale); this.updateLocale();
} }
updateLocale(requestedLocale: string | undefined = undefined) { updateLocale() {
const localeRequest = autoDetectLanguage(requestedLocale, this.brand?.defaultLocale); const localeRequest = autoDetectLanguage(this.userLocale, this.brand?.defaultLocale);
const locale = getBestMatchLocale(localeRequest); const locale = getBestMatchLocale(localeRequest);
if (!locale) { if (!locale) {
console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`); console.warn(`authentik/locale: failed to find locale for code ${localeRequest}`);

View File

@ -16,7 +16,6 @@ export interface RadioOption<T> {
description?: TemplateResult; description?: TemplateResult;
default?: boolean; default?: boolean;
value: T; value: T;
disabled?: boolean;
} }
@customElement("ak-radio") @customElement("ak-radio")
@ -78,9 +77,6 @@ export class Radio<T> extends CustomEmitterElement(AKElement) {
// This is a controlled input. Stop the native event from escaping or affecting the // This is a controlled input. Stop the native event from escaping or affecting the
// value. We'll do that ourselves. // value. We'll do that ourselves.
ev.stopPropagation(); ev.stopPropagation();
if (option.disabled) {
return;
}
this.value = option.value; this.value = option.value;
this.dispatchCustomEvent("change", { value: option.value }); this.dispatchCustomEvent("change", { value: option.value });
this.dispatchCustomEvent("input", { value: option.value }); this.dispatchCustomEvent("input", { value: option.value });
@ -97,7 +93,6 @@ export class Radio<T> extends CustomEmitterElement(AKElement) {
name="${this.name}" name="${this.name}"
id=${elId} id=${elId}
.checked=${option.value === this.value} .checked=${option.value === this.value}
.disabled=${option.disabled}
/> />
<label class="pf-c-radio__label" for=${elId}>${option.label}</label> <label class="pf-c-radio__label" for=${elId}>${option.label}</label>
${option.description ${option.description

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