Compare commits

..

49 Commits

Author SHA1 Message Date
b865ed4392 web: keeping up with the navigational changes mandated by brands and anterprise 2024-06-13 13:16:40 -07:00
f0afac0b87 Merge branch 'main' into web/sidebar-with-live-content-3
* main: (1301 commits)
  website/developer-docs: add a baby Style Guide (#9900)
  website/integrations: gitlab: update certificate key pair location and specify sha (#9925)
  root: handle asgi exception (#10085)
  website: bump prettier from 3.3.1 to 3.3.2 in /website (#10082)
  web: bump prettier from 3.3.1 to 3.3.2 in /web (#10081)
  core: bump google-api-python-client from 2.132.0 to 2.133.0 (#10083)
  web: bump prettier from 3.3.1 to 3.3.2 in /tests/wdio (#10079)
  web: bump chromedriver from 125.0.3 to 126.0.0 in /tests/wdio (#10078)
  web: bump @sentry/browser from 8.8.0 to 8.9.1 in /web in the sentry group (#10080)
  web: bump braces from 3.0.2 to 3.0.3 in /web (#10077)
  website: bump braces from 3.0.2 to 3.0.3 in /website (#10076)
  web: bump braces from 3.0.2 to 3.0.3 in /tests/wdio (#10075)
  core: bump azure-identity from 1.16.0 to 1.16.1 (#10071)
  rbac: filters: fix missing attribute for unauthenticated requests (#10061)
  tests/e2e: docker-compose.yml: remove version element forgotten last time (#10067)
  providers/microsoft_entra: fix error when updating connection attributes (#10039)
  website/integrations: aws: fix about service link (#10062)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in it (#10060)
  core: bump github.com/redis/go-redis/v9 from 9.5.2 to 9.5.3 (#10046)
  core: bump github.com/gorilla/websocket from 1.5.1 to 1.5.2 (#10047)
  ...
2024-06-13 11:06:19 -07:00
a31588668d Not sure how a messed-up version of this got into this commit, but it needs to go. 2024-01-08 14:20:17 -08:00
9768684c3c Merge branch 'dev' into web/sidebar-with-live-content-3
* dev: (131 commits)
  web: Replace  calls to `rootInterface()?.tenant?` with a contextual `this.tenant` object (#7778)
  web: abstract `rootInterface()?.config?.capabilities.includes()` into `.can()` (#7737)
  web: update some locale details (#8090)
  web: bump the eslint group in /web with 2 updates (#8082)
  web: bump rollup from 4.9.2 to 4.9.4 in /web (#8083)
  core: bump github.com/redis/go-redis/v9 from 9.3.1 to 9.4.0 (#8085)
  web: bump the eslint group in /tests/wdio with 2 updates (#8086)
  website: bump @types/react from 18.2.46 to 18.2.47 in /website (#8088)
  stages/user_login: only set last_ip in session if a binding is given (#8074)
  providers/oauth2: fix missing nonce in token endpoint not being saved (#8073)
  core: bump goauthentik.io/api/v3 from 3.2023105.3 to 3.2023105.5 (#8066)
  providers/oauth2: fix missing nonce in id_token (#8072)
  rbac: fix error when looking up permissions for now uninstalled apps (#8068)
  web/flows: fix device picker incorrect foreground color (#8067)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#8061)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#8062)
  website: bump postcss from 8.4.32 to 8.4.33 in /website (#8063)
  web: bump the sentry group in /web with 2 updates (#8064)
  core: bump golang.org/x/sync from 0.5.0 to 0.6.0 (#8065)
  website/docs: add link to our example flows (#8052)
  ...
2024-01-08 13:44:49 -08:00
cde94c2377 Merge branch 'main' into web/sidebar-with-live-content-3
* main:
  web: dark/light theme fixes (#7872)
  web: replace 'description-list' with list of descriptions (#7392)
  web: expressing success (#7830)
  web: fix turnstile types after update (#7854)
  core: bump github.com/google/uuid from 1.4.0 to 1.5.0 (#7866)
  website: bump @types/react from 18.2.43 to 18.2.45 in /website (#7865)
  web: bump wdio-wait-for from 3.0.9 to 3.0.10 in /tests/wdio (#7867)
  website/blog: okta part two blog (#7863)
  web: bump lit-analyzer from 2.0.1 to 2.0.2 in /web (#7858)
  web: bump the babel group in /web with 4 updates (#7856)
  web: bump the eslint group in /web with 2 updates (#7857)
  web: bump rollup from 4.7.0 to 4.8.0 in /web (#7859)
  web: bump the eslint group in /tests/wdio with 2 updates (#7860)
  web: refactor the table renderer for legibility  (#7433)
  documentation: Improve explanation of `kubernetes_json_patches` (#7832)
  root: update security policy to include link to cure53 report (#7853)
2023-12-13 11:25:41 -08:00
b0e852afca Merge branch 'main' into web/sidebar-with-live-content-3
* main:
  scripts: postgres, redis: only listen on localhost (#7849)
  website: bump @types/react from 18.2.42 to 18.2.43 in /website (#7840)
  web: bump ts-node from 10.9.1 to 10.9.2 in /tests/wdio (#7846)
  core: bump github.com/go-openapi/runtime from 0.26.0 to 0.26.2 (#7841)
  website: bump prettier from 3.1.0 to 3.1.1 in /website (#7839)
  web: bump the esbuild group in /web with 2 updates (#7842)
  web: bump rollup from 4.6.1 to 4.7.0 in /web (#7843)
  web: bump prettier from 3.1.0 to 3.1.1 in /web (#7844)
  web: bump the wdio group in /tests/wdio with 2 updates (#7845)
  web: bump prettier from 3.1.0 to 3.1.1 in /tests/wdio (#7847)
  translate: Updates for file web/xliff/en.xlf in fr (#7851)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in fr (#7850)
  core: bump python from 3.12.0-slim-bookworm to 3.12.1-slim-bookworm
  web/flows: update flow background (#7833)
  web/flows: fix logo height (#7834)
  Fix cache related image build issues
2023-12-11 08:28:43 -08:00
e35cefb63e Just a little clean-up. 2023-12-08 12:38:51 -08:00
2a11356961 Merge branch 'dev' into web/sidebar-with-live-content-3
* dev: (72 commits)
  web/flows: show logo in card (#7824)
  blueprints: improve file change handler (#7813)
  web/user: fix search not updating app (#7825)
  web: bump the storybook group in /web with 5 updates (#7819)
  core: compile backend translations (#7827)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in de (#7812)
  core: bump github.com/go-openapi/strfmt from 0.21.8 to 0.21.9 (#7814)
  ci: bump actions/stale from 8 to 9 (#7815)
  web: bump the wdio group in /tests/wdio with 1 update (#7816)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7820)
  web: bump the sentry group in /web with 2 updates (#7817)
  web: bump vite-tsconfig-paths from 4.2.1 to 4.2.2 in /web (#7818)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7821)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh-Hans (#7822)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_CN (#7823)
  web: bump typescript from 5.3.2 to 5.3.3 in /web (#7806)
  website: bump typescript from 5.3.2 to 5.3.3 in /website (#7807)
  web: bump typescript from 5.3.2 to 5.3.3 in /tests/wdio (#7808)
  core: bump goauthentik.io/api/v3 from 3.2023104.1 to 3.2023104.2 (#7809)
  ci: bump actions/setup-go from 4 to 5
  ...
2023-12-08 12:26:33 -08:00
a3673906c7 web: provide three-tier sidebar with subcategories
This commit implements a three-tier sidebar with subcategories.

The first thing is that we've refactored the Sidebar into a holistic entity; rather than be built in
pieces, it's constructed declaratively from a tree of entries, much in the same way routes are, and
for much the same reason<sup>1</sup>.

The AdminSidebar element only provides the list of entries to show and some of the controls
necessary to show/hide the sidebar.  Because the sidebar requires a rich collection of objects
retrieved from the back-end, to avoid cluttering the AdminSidebar each of those sublists of
TypeCreate have been isolated into their own controllers.

The SidebarTypeController isn't even a strongly reactive controller; all it does is fetch the
TypeCreate collection and notify the client object that the fetch has completed. The client can then
call the `.entries()` method on the controller to get the sub-tree of entries for the TypeCreate
object.

The Sidebar has been slightly (!) refactored so that it's emphatic about what it does: it shows the
brand, nav, and user sections of the sidebar. The styling has been moved to a separate file, the
better to emphasize this.

The SidebarItems file is where all the magic-- and a lot of ugly-- is hidden.

The main purpose of the SidebarItems is to render the tree of entries passed to it.  That's it.  But
it also has to be able to read the URL and highlight which entry is currently being shown by the
router, and it has to be able to open up all the parent objects of the "current" link so that the
user gets a clear sense of where they are navigationally.

Most messy: the `reclick()` function intercepts clicks on anchors and, using the same logic as the
router, decides if the router would *not* handle the navigation event.  If the router would not, it
takes on the responsibility for reaching into the currently visible table, modifying the filter and
triggering a new `.fetch()`.

Somewhere along the way I boyscoutted another `switch` statement or two into lookup expressions.

---

<sup>1</sup>&nbsp; One of the reasons for this is that the Administrator Application's sidebar,
routes, and command palette will all get their data from a single source of truth, and that single
source will be independent of any of those.  This is a step in that direction.
2023-11-28 11:11:35 -08:00
f2834cc7e2 web: provide tier-3 click capability.
This commit enables the search function to work when changing from one tier 3 link to another in
the tier 2 category (Providers, Events, Stages, etc.).  Because the router ignores such changes,
we must bring those changes to the attention of the receiver by hand.

This commit makes the recepient Table object locatable with a data attribute tag and a painstakingly
researched path to its most common location in our code.

It then checks any `anchor:click` event from the Sidebar to see if its likely to be elided
by the router.  If it is, we take over, identify the Table object, set or clear its search
state to the desired state, and call the `.fetch()` method on the Table to initiate a new
search with that state.

This is, frankly, _gross_.  The URL and internal states mirror the actual desired state more
or less by accident.  But it's what we've got to work with at the moment.
2023-11-27 15:48:24 -08:00
5b898bef01 Merge branch 'main' into web/sidebar-with-live-content-3
* main: (47 commits)
  web: bump the wdio group in /tests/wdio with 2 updates (#7702)
  events: fix lint (#7700)
  events: add better fallback for sanitize_item to ensure everything can be saved as JSON (#7694)
  web: bump the wdio group in /tests/wdio with 4 updates (#7696)
  events: include user agent in events (#7693)
  web: fix labels on group view page (#7677)
  website/docs: Add OIDC auth integration with Nextcloud (#7406)
  web: fix locale (#7689)
  core: bump python from 3.11.5-bookworm to 3.12.0-bookworm (#7048)
  translate: Updates for file web/xliff/en.xlf in zh_TW (#7688)
  web: bump pyright from 1.1.336 to 1.1.337 in /web (#7681)
  core: bump sentry-sdk from 1.35.0 to 1.36.0 (#7683)
  website: bump prism-react-renderer from 2.2.0 to 2.3.0 in /website (#7685)
  web: bump the sentry group in /web with 2 updates (#7679)
  web: bump rollup from 4.5.0 to 4.5.1 in /web (#7680)
  web: bump @types/codemirror from 5.60.14 to 5.60.15 in /web (#7682)
  web: bump the wdio group in /tests/wdio with 2 updates (#7684)
  website: bump react-tooltip from 5.23.0 to 5.24.0 in /website (#7686)
  core: bump goauthentik.io/api/v3 from 3.2023103.4 to 3.2023104.1 (#7687)
  website/blog: Blog on security (#7671)
  ...
2023-11-27 15:07:10 -08:00
6b9201907d Merge branch 'dev' into web/sidebar-with-live-content-3
* dev: (21 commits)
  sources/ldap: clean-up certs written from db (#7617)
  web: bump the eslint group in /tests/wdio with 1 update (#7635)
  core: compile backend translations (#7637)
  core: bump psycopg from 3.1.12 to 3.1.13 (#7625)
  core: bump ruff from 0.1.5 to 0.1.6 (#7626)
  core: bump twilio from 8.10.1 to 8.10.2 (#7627)
  web: bump the eslint group in /web with 1 update (#7629)
  web: bump the esbuild group in /web with 2 updates (#7630)
  web: bump rollup from 4.4.1 to 4.5.0 in /web (#7631)
  web: bump core-js from 3.33.2 to 3.33.3 in /web (#7633)
  core: bump goauthentik.io/api/v3 from 3.2023103.3 to 3.2023103.4 (#7634)
  web: bump the wdio group in /tests/wdio with 4 updates (#7636)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW (#7628)
  root: specify node and python versions in respective config files, deduplicate in CI (#7620)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7619)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7618)
  tests: better per-test timeouts (#7612)
  web: bump API Client version (#7613)
  stages/identification: add option to pretend user exists (#7610)
  events: stop spam (#7611)
  ...
2023-11-20 07:51:04 -08:00
2ec8932891 Merge branch 'main' into dev
* main: (63 commits)
  sources/ldap: clean-up certs written from db (#7617)
  web: bump the eslint group in /tests/wdio with 1 update (#7635)
  core: compile backend translations (#7637)
  core: bump psycopg from 3.1.12 to 3.1.13 (#7625)
  core: bump ruff from 0.1.5 to 0.1.6 (#7626)
  core: bump twilio from 8.10.1 to 8.10.2 (#7627)
  web: bump the eslint group in /web with 1 update (#7629)
  web: bump the esbuild group in /web with 2 updates (#7630)
  web: bump rollup from 4.4.1 to 4.5.0 in /web (#7631)
  web: bump core-js from 3.33.2 to 3.33.3 in /web (#7633)
  core: bump goauthentik.io/api/v3 from 3.2023103.3 to 3.2023103.4 (#7634)
  web: bump the wdio group in /tests/wdio with 4 updates (#7636)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW (#7628)
  root: specify node and python versions in respective config files, deduplicate in CI (#7620)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7619)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7618)
  tests: better per-test timeouts (#7612)
  web: bump API Client version (#7613)
  stages/identification: add option to pretend user exists (#7610)
  events: stop spam (#7611)
  ...
2023-11-20 07:50:46 -08:00
a9886b047e web: further refinements to the sidebar
This commit restores the onHashChange functionality, using an
on-demand reverse map (there really aren't that many objects in the
nav tree) to make sure all of the parent entities are also listed
in the "expanded" listing to make sure the target object is still
visible.  Along the way, several type lever errors were corrected.
Two major pieces of functionality were extracted from the Sidebar
function as they're mostly consumers/filters of the information
provided, and don't need to be in the Sidebar itself.
2023-11-17 14:47:47 -08:00
a0dfe7ce78 fixed bug where the record could get lost. 2023-11-17 11:44:24 -08:00
c471428c6b Revert to a prior version; fix bottom border so tier-2 elements with children don't have a separator, as before. 2023-11-17 11:34:51 -08:00
83e934f80c Removed the consoe.log 2023-11-17 11:18:43 -08:00
5386f0f4c3 Streamline TypeCreate lists. This commit removes the highly repetitive definitions for each
of the TypeCreate objects and replaces them with a single generic ReactiveController, which it
then instantiates six times, but at least they're shorter!
2023-11-17 11:15:58 -08:00
d5875a597b Merge branch 'main' into web/sidebar-with-live-content-3
* main: (42 commits)
  stages/authenticator_totp: fix API validation error due to choices (#7608)
  website: fix pricing page inconsistency (#7607)
  web: bump API Client version (#7602)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7603)
  core: bump goauthentik.io/api/v3 from 3.2023103.2 to 3.2023103.3 (#7606)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7604)
  Revert "web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web (#7486)"
  root: fix API schema for kotlin (#7601)
  web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web (#7486)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7583)
  events: fix missing model_* events when not directly authenticated (#7588)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW (#7594)
  providers/scim: fix missing schemas attribute for User and Group (#7477)
  core: bump pydantic from 2.5.0 to 2.5.1 (#7592)
  web/admin: contextually add user to group when creating user from group page (#7586)
  website/blog: title and slug change (#7585)
  events: sanitize functions (#7587)
  stages/email: use uuid for email confirmation token instead of username (#7581)
  website/blog: Blog about zero trust and wireguard (#7567)
  ci: translation-advice: avoid commenting after make i18n-extract
  ...
2023-11-17 09:34:15 -08:00
25ecc21d6d Merge branch 'dev' into web/sidebar-with-live-content-3
* dev:
2023-11-17 09:34:09 -08:00
ff78f2f00a web: almost there with the sidebar
The actual behavior is more or less what I expected.  What's missing is:

- Persistence of location (the hover effect fades with a click anywhere else)
- Proper testing of the oddities
- Full (or any!) responsiveness when moving between third-tier links in the same category

Stretch goal:
- Remembering the state of the sidebar when transitioning between the user and the admin (this will require using some localstorage, I suspect).

I also think that in my rush there's a bit of lost internal coherency.  I'd like to figure out what's wiggling around my brain and solve that discomfort.
2023-11-16 14:59:02 -08:00
3c277f14c8 web: sidebar third tier
The third tier works!  The only problem is that route isn't responsive, and I'm not sure why.
If you leave the `Providers` and go somewhere else, then click on a third-tier, the filter
works fine.  But if you click on one third-tier and then another, the filter doesn't
change.  Must investigate.
2023-11-16 11:03:16 -08:00
d539884204 web: continuing with the Sidebar
I've finally reached a stage where I have a framework I can build upon, but what
a pain in the posterior it was to get here.  Keeping the entire navigation list
within a single DOM is a solid idea, but porting from the original code to this
proved to be unreasonably kludegy.  Instead, I started from scratch, adding each
step along the way, a sort of Transformation Priority Premise, and testing each
step to make sure it was all behaving as expected.

So far, so good.

The remaining details are not trivial: I have to figure out how to express the
different classes of actions, and get the third tier working, but at least
the React version gives us hints.
2023-11-16 10:38:36 -08:00
476adef4ea Merge branch 'main' into web/sidebar-with-live-content-2
* main: (24 commits)
  internal: remove special route for /outpost.goauthentik.io (#7539)
  providers/proxy: Fix duplicate cookies when using file system store. (#7541)
  web: bump API Client version (#7543)
  sources/ldap: add check command to verify ldap connectivity (#7263)
  internal: remove deprecated metrics (#7540)
  core: compile backend translations (#7538)
  web: bump prettier from 3.0.3 to 3.1.0 in /web (#7528)
  web: bump @trivago/prettier-plugin-sort-imports from 4.2.1 to 4.3.0 in /web (#7531)
  web: bump rollup from 4.3.0 to 4.4.0 in /web (#7529)
  core: bump celery from 5.3.4 to 5.3.5 (#7536)
  web: bump @formatjs/intl-listformat from 7.5.1 to 7.5.2 in /web (#7530)
  web: bump prettier from 3.0.3 to 3.1.0 in /tests/wdio (#7532)
  web: bump @trivago/prettier-plugin-sort-imports from 4.2.1 to 4.3.0 in /tests/wdio (#7533)
  website: bump prettier from 3.0.3 to 3.1.0 in /website (#7534)
  website: bump prism-react-renderer from 2.1.0 to 2.2.0 in /website (#7535)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW (#7537)
  root: Restructure broker / cache / channel / result configuration (#7097)
  core: bump twilio from 8.10.0 to 8.10.1 (#7474)
  web: bump axios from 1.5.0 to 1.6.1 in /web (#7518)
  web: bump wdio-wait-for from 3.0.7 to 3.0.8 in /tests/wdio (#7514)
  ...
2023-11-14 10:25:04 -08:00
3e905cc956 web: refactor sidebar capabilities for categorical subsections
Move open/close logic into the ak-admin-sidebar itself.

This commit removes the responsibility for opening/closing the sidebar from the interface parent
code and places it inside the sidebar entirely.  Since the Django invocation passes none of the
properties ak-interface-admin is capable of receiving, this seems like a safe operation.

The sidebar now assumes the responsibility for hooking up the window event listeners for open/close
and resize.

On connection to the DOM, and on resize, the sidebar checks to see if the viewport width meets the
criteria for a behavioral change (slide-overlay vs slide-push), and on slide-push automatically
opens the sidebar on the assumption that there's plenty of room. In order to support more dynamic
styling going forward, I've substituted the 1280px with 80rem, which is the same, but allows for
some better styling if someone with older eyes needs to "zoom in" on the whole thing with a larger
font size.

The hide/show code involves "reaching up" to touch the host's classList.  There's a comment
indicating that this is a slightly fragile thing to do, but in a well-known way.
2023-11-13 15:36:39 -08:00
e3b1ba63a6 Function to help generate sizing solutions across Javascript and CSS. 2023-11-13 14:53:05 -08:00
2aed74bd9f Merge branch 'main' into dev
* main: (24 commits)
  internal: remove special route for /outpost.goauthentik.io (#7539)
  providers/proxy: Fix duplicate cookies when using file system store. (#7541)
  web: bump API Client version (#7543)
  sources/ldap: add check command to verify ldap connectivity (#7263)
  internal: remove deprecated metrics (#7540)
  core: compile backend translations (#7538)
  web: bump prettier from 3.0.3 to 3.1.0 in /web (#7528)
  web: bump @trivago/prettier-plugin-sort-imports from 4.2.1 to 4.3.0 in /web (#7531)
  web: bump rollup from 4.3.0 to 4.4.0 in /web (#7529)
  core: bump celery from 5.3.4 to 5.3.5 (#7536)
  web: bump @formatjs/intl-listformat from 7.5.1 to 7.5.2 in /web (#7530)
  web: bump prettier from 3.0.3 to 3.1.0 in /tests/wdio (#7532)
  web: bump @trivago/prettier-plugin-sort-imports from 4.2.1 to 4.3.0 in /tests/wdio (#7533)
  website: bump prettier from 3.0.3 to 3.1.0 in /website (#7534)
  website: bump prism-react-renderer from 2.1.0 to 2.2.0 in /website (#7535)
  translate: Updates for file locale/en/LC_MESSAGES/django.po in zh_TW (#7537)
  root: Restructure broker / cache / channel / result configuration (#7097)
  core: bump twilio from 8.10.0 to 8.10.1 (#7474)
  web: bump axios from 1.5.0 to 1.6.1 in /web (#7518)
  web: bump wdio-wait-for from 3.0.7 to 3.0.8 in /tests/wdio (#7514)
  ...
2023-11-13 09:07:42 -08:00
2545815f08 Merge branch 'main' into web/sidebar-with-live-content
* main: (23 commits)
  web: bump API Client version (#7513)
  web: bump the sentry group in /web with 2 updates (#7500)
  core: bump pytest-django from 4.6.0 to 4.7.0 (#7497)
  core: bump black from 23.10.1 to 23.11.0 (#7498)
  core: bump ruff from 0.1.4 to 0.1.5 (#7499)
  core: bump golang.org/x/oauth2 from 0.13.0 to 0.14.0 (#7501)
  web: bump the wdio group in /tests/wdio with 3 updates (#7502)
  release: 2023.10.3
  ci: fix permissions for release pipeline to publish binaries (#7512)
  website/docs: update release notes for 2023.10.3
  core: fix worker beat toggle inverted (#7508)
  website/docs: update release notes for 2023.10.3
  website/docs: fix anchor link (#7492)
  consistent variable name
  add more tooltips and add device authn/authz
  fix wrong color
  events: fix gdpr compliance always running
  website/docs: Fix a small grammar issue (#7490)
  core: bump golang from 1.21.3-bookworm to 1.21.4-bookworm
  web: bump pyright from 1.1.334 to 1.1.335 in /web
  ...
2023-11-09 13:17:58 -08:00
657089eac9 Merge branch 'main' into dev
* main: (23 commits)
  web: bump API Client version (#7513)
  web: bump the sentry group in /web with 2 updates (#7500)
  core: bump pytest-django from 4.6.0 to 4.7.0 (#7497)
  core: bump black from 23.10.1 to 23.11.0 (#7498)
  core: bump ruff from 0.1.4 to 0.1.5 (#7499)
  core: bump golang.org/x/oauth2 from 0.13.0 to 0.14.0 (#7501)
  web: bump the wdio group in /tests/wdio with 3 updates (#7502)
  release: 2023.10.3
  ci: fix permissions for release pipeline to publish binaries (#7512)
  website/docs: update release notes for 2023.10.3
  core: fix worker beat toggle inverted (#7508)
  website/docs: update release notes for 2023.10.3
  website/docs: fix anchor link (#7492)
  consistent variable name
  add more tooltips and add device authn/authz
  fix wrong color
  events: fix gdpr compliance always running
  website/docs: Fix a small grammar issue (#7490)
  core: bump golang from 1.21.3-bookworm to 1.21.4-bookworm
  web: bump pyright from 1.1.334 to 1.1.335 in /web
  ...
2023-11-09 10:53:55 -08:00
19e8b675ae web: refactor sidebar capabilities for categorical subsections
The project "Change Admin UI lists to have sublists per type" requires some initial changes to the
UI to facilitate this request. The AdminSidebar is the principle target of this project, and it is
embedded in the AdminInterface. To facilitate editing the AdminSidebar as an independent entity,
AdminInterface has been moved into its own folder and the AdminSidebar extracted as a standalone Web
Component. This removes, oh, about half the code from AdminInterface. A little cleanup with
`classMap` was also committed.

The rollup config was adjusted to find the new AdminInterface location.

The Sidebar uses the global `config: Config` object to check for Enterprise capabilities. Rather
than plumb all the way down through the Interface => AdminInterface -> AdminSidebar, I chose to make
provide an alternative way of reaching the `config` object, as a *context*. Other configuration
objects (Me, UiConfig, Tenant) interfaces will be contextualized as demand warrants.

Demand will warrant.  Just not yet. <sup>1</sup>

The Sidebar has been refactored only slightly; the renderers are entirely the same as they were
prior to extraction. What has been changed is the source of information: when we retrieve the
current version we story *only* the information, and use type information to ensure that the version
we store is the version we care about. The same is true of `impersonation`; we care only about the
name of the person being impersonated being present, so we don't store anything else.

Fetches have been moved from `firstUpdated` to the constructor.  No reason to have the sidebar
render twice if the network returns before the render is scheduled.

Because the path used to identify the user being impersonated has changed, the `str()` references in
the XLIFF files had to be adjusted. **This change is to a variable only and does not require
translation.**

---
<sup>1</sup> The code is littered with checks to `me()?`, `uiConfig?`, `config?`, etc. In the
*context* of being logged in as an administrator those should never be in doubt. I intend to make
our interfaces not have any doubt.
2023-11-07 14:13:20 -08:00
bdd92f63d8 Revert "Due for amendment"
This reverts commit 829ad5d3f2.
2023-11-07 13:37:00 -08:00
829ad5d3f2 Due for amendment 2023-11-07 13:29:53 -08:00
58639a5d03 Merge branch 'main' into dev
* main:
  Web: bugfix: broken backchannel selector (#7480)
  web: rollback dependabot context (#7479)
2023-11-07 10:11:01 -08:00
67cae13f93 web: rollback dependabot's upgrade of context
The most frustrating part of this is that I RAN THIS, dammit, with the updated
context and the current Wizard, and it finished the End-to-End tests without
complaint.
2023-11-07 07:45:28 -08:00
100a6f02f1 Merge branch 'main' into dev
* main:
  web: bump @types/chart.js from 2.9.39 to 2.9.40 in /web
  website/integrations: add FreshRSS (#7301)
  web: bump the eslint group in /web with 2 updates
  core: bump uvicorn from 0.24.0 to 0.24.0.post1
  web: bump the storybook group in /web with 5 updates
  web: bump the eslint group in /tests/wdio with 2 updates
  web: bump @types/codemirror from 5.60.12 to 5.60.13 in /web
  web: bump mermaid from 10.6.0 to 10.6.1 in /web
  translate: Updates for file web/xliff/en.xlf in fr (#7461)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7459)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7458)
  web: bump @lit/localize-tools from 0.7.0 to 0.7.1 in /web (#7369)
  web: bump @lit-labs/context from 0.4.1 to 0.5.1 in /web (#7368)
2023-11-07 07:34:30 -08:00
242e5b492b Merge branch 'main' into dev
* main:
  web/flows: attempt to fix bitwareden android compatibility (#7455)
  sources/oauth: fix patreon (#7454)
  website: bump the docusaurus group in /website with 3 updates (#7400)
  web/admin: fix chart label on dashboard user page (#7434)
  core: bump github.com/gorilla/sessions from 1.2.1 to 1.2.2 (#7446)
  core: bump github.com/gorilla/mux from 1.8.0 to 1.8.1 (#7443)
  core: bump github.com/spf13/cobra from 1.7.0 to 1.8.0 (#7442)
  core: bump github.com/gorilla/websocket from 1.5.0 to 1.5.1 (#7445)
  core: bump golang.org/x/sync from 0.4.0 to 0.5.0 (#7441)
  core: bump github.com/gorilla/securecookie from 1.1.1 to 1.1.2 (#7440)
  core: bump github.com/gorilla/handlers from 1.5.1 to 1.5.2 (#7444)
  web: bump rollup from 4.2.0 to 4.3.0 in /web (#7448)
  web: bump the eslint group in /web with 2 updates (#7447)
  core: bump uvicorn from 0.23.2 to 0.24.0 (#7450)
  core: bump selenium from 4.15.1 to 4.15.2 (#7449)
  core: bump ruff from 0.1.3 to 0.1.4 (#7451)
  web: bump the eslint group in /tests/wdio with 2 updates (#7452)
2023-11-06 09:40:19 -08:00
48495f3c53 Merge branch 'main' into dev
* main:
  providers/proxy: fix closed redis client (#7385)
  ci: explicitly give write permissions to packages (#7428)
  core: bump selenium from 4.15.0 to 4.15.1 (#7422)
  web: bump yaml from 2.3.3 to 2.3.4 in /web (#7420)
  core: bump sentry-sdk from 1.33.1 to 1.34.0 (#7421)
  web: bump the wdio group in /tests/wdio with 4 updates (#7423)
  providers/oauth2: set auth_via for token and other endpoints (#7417)
  website/blog: draft for happy bday blog (#7408)
2023-11-03 08:45:02 -07:00
77549753c2 Merge branch 'main' into dev
* main:
  translate: Updates for file web/xliff/en.xlf in fr (#7416)
  website: bump react-tooltip from 5.21.6 to 5.22.0 in /website (#7412)
  core: bump selenium from 4.14.0 to 4.15.0 (#7411)
  core: bump django from 4.2.6 to 4.2.7 (#7413)
  web: bump the eslint group in /web with 1 update (#7414)
  web: bump the eslint group in /tests/wdio with 1 update (#7415)
  root: Improve multi arch Docker image build speed (#7355)
2023-11-02 08:15:01 -07:00
3b19aa1915 Merge branch 'main' into dev
* main:
  website/integrations: argocd: add missing url in ArgoCD configuration (#7404)
  core: bump sentry-sdk from 1.32.0 to 1.33.1 (#7397)
  core: bump webauthn from 1.11.0 to 1.11.1 (#7399)
  core: bump github.com/redis/go-redis/v9 from 9.2.1 to 9.3.0 (#7396)
  core: bump twisted from 23.8.0 to 23.10.0 (#7398)
  web: bump the sentry group in /web with 2 updates (#7401)
  web: bump pyright from 1.1.333 to 1.1.334 in /web (#7402)
  web: bump rollup from 4.1.5 to 4.2.0 in /web (#7403)
  core: bump pytest-django from 4.5.2 to 4.6.0 (#7387)
  web: bump the eslint group in /tests/wdio with 2 updates (#7388)
  web: bump the sentry group in /web with 2 updates (#7366)
  web: bump the eslint group in /web with 2 updates (#7389)
  web: bump core-js from 3.33.1 to 3.33.2 in /web (#7390)
  stages/email: fix duplicate querystring encoding (#7386)
  web/admin: fix html error on oauth2 provider page (#7384)
2023-11-01 09:57:21 -07:00
6653bd8224 Merge branch 'main' into dev
* main:
  web: bump rollup from 4.1.4 to 4.1.5 in /web (#7370)
  website/integrations: add SonarQube (#7167)
  web: bump the storybook group in /web with 5 updates (#7382)
  core: bump goauthentik.io/api/v3 from 3.2023101.1 to 3.2023102.1 (#7378)
  web: bump ts-lit-plugin from 2.0.0 to 2.0.1 in /web (#7379)
  web: bump @rollup/plugin-replace from 5.0.4 to 5.0.5 in /web (#7380)
  web: bump API Client version (#7365)
  website/docs: add 2023.8.4 release notes
  release: 2023.10.2
  security: fix oobe-flow reuse when akadmin is deleted (#7361)
  website/docs: prepare 2023.10.2 release notes (#7362)
  website/docs: add missing breaking change due to APPEND_SLASH (#7360)
  lifecycle: rework otp_merge migration (#7359)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7354)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7353)
  website/docs: add warning about Helm breaking change in 2024.x (#7351)
2023-10-30 08:51:07 -07:00
639a8ceb5a Merge branch 'main' into dev
* main: (38 commits)
  crypto: fix race conditions when creating self-signed certificates on startup (#7344)
  blueprints: fix entries with state: absent not being deleted if their serializer has errors (#7345)
  web/admin: fix @change handler for ak-radio elements (#7348)
  rbac: handle lookup error (#7341)
  website/docs: add warning about upgrading to 2023.10 (#7340)
  web/admin: fix role form reacting to enter (#7330)
  core: bump github.com/google/uuid from 1.3.1 to 1.4.0 (#7333)
  core: bump goauthentik.io/api/v3 from 3.2023083.10 to 3.2023101.1 (#7334)
  core: bump ruff from 0.1.2 to 0.1.3 (#7335)
  core: bump pydantic-scim from 0.0.7 to 0.0.8 (#7336)
  website/blogs: Blog dockers (#7328)
  providers/proxy: attempt to fix duplicate cookie (#7324)
  stages/email: fix sending emails from task (#7325)
  web: bump API Client version (#7321)
  website/docs: update release notes for 2023.10.1 (#7316)
  release: 2023.10.1
  lifecycle: fix otp merge migration (#7315)
  root: fix pylint errors (#7312)
  web: bump API Client version (#7311)
  release: 2023.10.0
  ...
2023-10-27 09:47:08 -07:00
0449fd07c5 Merge branch 'main' into dev
* main: (28 commits)
  web: fix typo in traefik name
  web/admin: disable wizard banner for now (#7294)
  web/admin: small fixes (#7292)
  core: Use branding_title in the end session page (#7282)
  web: bump pyright from 1.1.332 to 1.1.333 in /web (#7287)
  website: bump react-tooltip from 5.21.5 to 5.21.6 in /website (#7283)
  web: bump the sentry group in /web with 2 updates (#7285)
  web: bump the eslint group in /web with 1 update (#7286)
  core: bump ruff from 0.1.1 to 0.1.2 (#7289)
  core: bump pytest from 7.4.2 to 7.4.3 (#7288)
  web: bump the wdio group in /tests/wdio with 3 updates (#7290)
  website/blogs: fixed typo in blog (#7281)
  core: bump pylint from 2.17.7 to 3.0.2 (#7270)
  web: bump the eslint group in /tests/wdio with 2 updates (#7274)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7278)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7277)
  ci: bump actions/setup-node from 3 to 4 (#7268)
  core: bump pylint-django from 2.5.4 to 2.5.5 (#7271)
  web: bump the eslint group in /web with 2 updates (#7269)
  web: bump @trivago/prettier-plugin-sort-imports from 4.2.0 to 4.2.1 in /tests/wdio (#7275)
  ...
2023-10-25 10:04:37 -07:00
8e892373a1 Merge branch 'main' into dev
* main:
  core: bump pylint-django from 2.5.3 to 2.5.4 (#7255)
  core: bump goauthentik.io/api/v3 from 3.2023083.9 to 3.2023083.10 (#7256)
  web: bump the wdio group in /tests/wdio with 1 update (#7258)
  web: bump the eslint group in /tests/wdio with 1 update (#7257)
  sources/oauth: fix name clash (#7253)
  web: bump the eslint group in /web with 1 update (#7250)
  web: bump mermaid from 10.5.0 to 10.5.1 in /web (#7247)
  web: break circular dependency between AKElement & Interface. (#7165)
2023-10-23 08:52:36 -07:00
8713a1d120 Merge branch 'main' into web/theme-controller-2
* main:
  sources/oauth: fix oidc well-known parsing (#7248)
  web/admin: improve user email button labels (#7233)
2023-10-20 14:12:57 -07:00
0123bf61ab web: fix broken typescript references
This built... and then it didn't?  Anyway, the current fix is to
provide type information the AkInterface for the data that consumers
require.
2023-10-20 10:31:44 -07:00
e8edbdb4ae Merge branch 'main' into web/theme-controller-2
* main:
  web: bump API Client version (#7246)
  sources/oauth: include default JWKS URLs for OAuth sources (#6992)
  sources/oauth: periodically update OAuth sources' OIDC configuration (#7245)
  website/blogs: Fix sso blog to remove 3rd reason (#7230)
  lifecycle: fix otp_merge migration again (#7244)
  web: bump core-js from 3.33.0 to 3.33.1 in /web (#7243)
  core: bump node from 20 to 21 (#7237)
  web: fix bad comment that was confusing lit-analyze (#7234)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7235)
  core: bump ruff from 0.1.0 to 0.1.1 (#7238)
  core: bump twilio from 8.9.1 to 8.10.0 (#7239)
  web: bump the storybook group in /web with 5 updates (#7240)
  web: bump the wdio group in /tests/wdio with 4 updates (#7241)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7236)
  web: isolate clipboard handling (#7229)
2023-10-20 08:28:00 -07:00
83338f8c32 Merge branch 'main' into web/theme-controller-2
* main:
  web/flows: update flow background (#7232)
  web/admin: fix prompt form and codemirror mode (#7231)
  web/admin: decrease wizard hint padding (#7227)
2023-10-19 16:28:34 -07:00
e51b36c614 Merge branch 'main' into web/theme-controller-2
* main: (57 commits)
  stages/email: Fix query parameters getting lost in Email links (#5376)
  core/rbac: fix missing field when removing perm, add delete from object page (#7226)
  website/integrations: grafana: add Helm and Terraform config examples (#7121)
  web: bump @types/codemirror from 5.60.11 to 5.60.12 in /web (#7223)
  translate: Updates for file web/xliff/en.xlf in zh_CN (#7224)
  translate: Updates for file web/xliff/en.xlf in zh-Hans (#7225)
  website/blogs: blog about sso tax (#7202)
  web: Application wizard v2 with tests (#7004)
  web: bump API Client version (#7220)
  core: bump goauthentik.io/api/v3 from 3.2023083.7 to 3.2023083.8 (#7221)
  providers/radius: TOTP MFA support (#7217)
  web: bump API Client version (#7218)
  stage/deny: add custom message (#7144)
  docs: update full-dev-setup docs (#7205)
  enterprise: bump license usage task frequency (#7215)
  web: bump the storybook group in /web with 5 updates (#7212)
  web: bump the sentry group in /web with 2 updates (#7211)
  Revert "web: Updates to the Context and Tasks libraries from lit. (#7168)"
  web: bump @types/codemirror from 5.60.10 to 5.60.11 in /web (#7209)
  web: bump @types/chart.js from 2.9.38 to 2.9.39 in /web (#7206)
  ...
2023-10-19 08:59:15 -07:00
314d89b1b7 web: break circular dependency between AKElement & Interface.
This commit changes the way the root node of the web application shell is
discovered by child components, such that the base class shared by both
no longer results in a circular dependency between the two models.

I've run this in isolation and have seen no failures of discovery; the identity
token exists as soon as the Interface is constructed and is found by every item
on the page.
2023-10-13 08:22:46 -07:00
391 changed files with 12109 additions and 21207 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2024.6.0
current_version = 2024.4.2
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)(?:-(?P<rc_t>[a-zA-Z-]+)(?P<rc_n>[1-9]\\d*))?
@ -17,8 +17,6 @@ optional_value = final
[bumpversion:file:pyproject.toml]
[bumpversion:file:package.json]
[bumpversion:file:docker-compose.yml]
[bumpversion:file:schema.yml]

View File

@ -21,10 +21,7 @@ updates:
labels:
- dependencies
- package-ecosystem: npm
directories:
- "/web"
- "/tests/wdio"
- "/web/sfe"
directory: "/web"
schedule:
interval: daily
time: "04:00"
@ -33,6 +30,7 @@ updates:
open-pull-requests-limit: 10
commit-message:
prefix: "web:"
# TODO: deduplicate these groups
groups:
sentry:
patterns:
@ -58,6 +56,38 @@ updates:
patterns:
- "@rollup/*"
- "rollup-*"
- package-ecosystem: npm
directory: "/tests/wdio"
schedule:
interval: daily
time: "04:00"
labels:
- dependencies
open-pull-requests-limit: 10
commit-message:
prefix: "web:"
# TODO: deduplicate these groups
groups:
sentry:
patterns:
- "@sentry/*"
- "@spotlightjs/*"
babel:
patterns:
- "@babel/*"
- "babel-*"
eslint:
patterns:
- "@typescript-eslint/*"
- "eslint"
- "eslint-*"
storybook:
patterns:
- "@storybook/*"
- "*storybook*"
esbuild:
patterns:
- "@esbuild/*"
wdio:
patterns:
- "@wdio/*"

View File

@ -31,12 +31,7 @@ jobs:
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
- name: Upgrade /web
working-directory: web
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION
- name: Upgrade /web/sfe
working-directory: web/sfe
working-directory: web/
run: |
export VERSION=`node -e 'console.log(require("../gen-ts-api/package.json").version)'`
npm i @goauthentik/api@$VERSION

View File

@ -219,7 +219,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -240,7 +240,7 @@ jobs:
- name: generate ts client
run: make gen-client-ts
- name: Build Docker Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v5
with:
context: .
secrets: |

View File

@ -76,7 +76,7 @@ jobs:
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -96,7 +96,7 @@ jobs:
- name: Generate API
run: make gen-client-go
- name: Build Docker Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v5
with:
tags: ${{ steps.ev.outputs.imageTags }}
file: ${{ matrix.type }}.Dockerfile

View File

@ -12,36 +12,14 @@ on:
- version-*
jobs:
lint:
lint-eslint:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- lint
- lint:lockfile
- tsc
- prettier-check
project:
- web
- tests/wdio
include:
- command: tsc
project: web
extra_setup: |
cd sfe/ && npm ci
- command: lit-analyse
project: web
extra_setup: |
# lit-analyse doesn't understand path rewrites, so make it
# belive it's an actual module
cd node_modules/@goauthentik
ln -s ../../src/ web
exclude:
- command: lint:lockfile
project: tests/wdio
- command: tsc
project: tests/wdio
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@ -50,17 +28,85 @@ jobs:
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
run: |
npm ci
${{ matrix.extra_setup }}
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: Lint
- name: Eslint
working-directory: ${{ matrix.project }}/
run: npm run ${{ matrix.command }}
run: npm run lint
lint-lockfile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- working-directory: web/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: TSC
working-directory: web/
run: npm run tsc
lint-prettier:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
project:
- web
- tests/wdio
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: ${{ matrix.project }}/package.json
cache: "npm"
cache-dependency-path: ${{ matrix.project }}/package-lock.json
- working-directory: ${{ matrix.project }}/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: prettier
working-directory: ${{ matrix.project }}/
run: npm run prettier-check
lint-lit-analyse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: |
npm ci
# lit-analyse doesn't understand path rewrites, so make it
# belive it's an actual module
cd node_modules/@goauthentik
ln -s ../../src/ web
- name: Generate API
run: make gen-client-ts
- name: lit-analyse
working-directory: web/
run: npm run lit-analyse
ci-web-mark:
needs:
- lint
- lint-lockfile
- lint-eslint
- lint-prettier
- lint-lit-analyse
- lint-build
runs-on: ubuntu-latest
steps:
- run: echo mark
@ -82,21 +128,3 @@ jobs:
- name: build
working-directory: web/
run: npm run build
test:
needs:
- ci-web-mark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: web/package.json
cache: "npm"
cache-dependency-path: web/package-lock.json
- working-directory: web/
run: npm ci
- name: Generate API
run: make gen-client-ts
- name: test
working-directory: web/
run: npm run test

View File

@ -12,21 +12,27 @@ on:
- version-*
jobs:
lint:
lint-lockfile:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
command:
- lint:lockfile
- prettier-check
steps:
- uses: actions/checkout@v4
- working-directory: website/
run: |
[ -z "$(jq -r '.packages | to_entries[] | select((.key | startswith("node_modules")) and (.value | has("resolved") | not)) | .key' < package-lock.json)" ]
lint-prettier:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: website/package.json
cache: "npm"
cache-dependency-path: website/package-lock.json
- working-directory: website/
run: npm ci
- name: Lint
- name: prettier
working-directory: website/
run: npm run ${{ matrix.command }}
run: npm run prettier-check
test:
runs-on: ubuntu-latest
steps:
@ -63,7 +69,8 @@ jobs:
run: npm run ${{ matrix.job }}
ci-website-mark:
needs:
- lint
- lint-lockfile
- lint-prettier
- test
- build
runs-on: ubuntu-latest

View File

@ -14,7 +14,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -40,7 +40,7 @@ jobs:
mkdir -p ./gen-ts-api
mkdir -p ./gen-go-api
- name: Build Docker Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v5
with:
context: .
push: true
@ -68,7 +68,7 @@ jobs:
with:
go-version-file: "go.mod"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: prepare variables
@ -94,7 +94,7 @@ jobs:
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker Image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.ev.outputs.imageTags }}
@ -155,8 +155,8 @@ jobs:
- uses: actions/checkout@v4
- name: Run test suite in final docker images
run: |
echo "PG_PASS=$(openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64 -w 0)" >> .env
echo "PG_PASS=$(openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 32 | base64)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis

View File

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

View File

@ -22,30 +22,20 @@ RUN npm run build-bundled
# Stage 2: Build webui
FROM --platform=${BUILDPLATFORM} docker.io/node:22 as web-builder
ARG GIT_BUILD_HASH
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
ENV NODE_ENV=production
WORKDIR /work/web
RUN --mount=type=bind,target=/work/web/package.json,src=./web/package.json \
--mount=type=bind,target=/work/web/package-lock.json,src=./web/package-lock.json \
--mount=type=bind,target=/work/web/sfe/package.json,src=./web/sfe/package.json \
--mount=type=bind,target=/work/web/sfe/package-lock.json,src=./web/sfe/package-lock.json \
--mount=type=bind,target=/work/web/scripts,src=./web/scripts \
--mount=type=cache,id=npm-web,sharing=shared,target=/root/.npm \
npm ci --include=dev && \
cd sfe && \
npm ci --include=dev
COPY ./package.json /work
COPY ./web /work/web/
COPY ./website /work/website/
COPY ./gen-ts-api /work/web/node_modules/@goauthentik/api
RUN npm run build && \
cd sfe && \
npm run build
RUN npm run build
# Stage 3: Build go proxy
FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.22-fips-bookworm AS go-builder

View File

@ -47,8 +47,8 @@ test-go:
go test -timeout 0 -v -race -cover ./...
test-docker: ## Run all tests in a docker-compose
echo "PG_PASS=$(shell openssl rand 32 | base64 -w 0)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64 -w 0)" >> .env
echo "PG_PASS=$(shell openssl rand 32 | base64)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(shell openssl rand 32 | base64)" >> .env
docker compose pull -q
docker compose up --no-start
docker compose start postgresql redis
@ -60,11 +60,9 @@ test: ## Run the server tests and produce a coverage report (locally)
coverage html
coverage report
lint-fix: lint-codespell ## Lint and automatically fix errors in the python source code. Reports spelling errors.
lint-fix: ## Lint and automatically fix errors in the python source code. Reports spelling errors.
black $(PY_SOURCES)
ruff check --fix $(PY_SOURCES)
lint-codespell: ## Reports spelling errors.
codespell -w $(CODESPELL_ARGS)
lint: ## Lint the python and golang sources
@ -241,7 +239,7 @@ website: website-lint-fix website-build ## Automatically fix formatting issues
website-install:
cd website && npm ci
website-lint-fix: lint-codespell
website-lint-fix:
cd website && npm run prettier
website-build:

View File

@ -18,10 +18,10 @@ Even if the issue is not a CVE, we still greatly appreciate your help in hardeni
(.x being the latest patch release for each version)
| Version | Supported |
| -------- | --------- |
| 2024.4.x | ✅ |
| 2024.6.x | ✅ |
| Version | Supported |
| --------- | --------- |
| 2023.10.x | ✅ |
| 2024.2.x | ✅ |
## Reporting a Vulnerability

View File

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

View File

@ -16,7 +16,6 @@ from rest_framework.views import APIView
from authentik import get_full_version
from authentik.core.api.utils import PassiveSerializer
from authentik.enterprise.license import LicenseKey
from authentik.lib.config import CONFIG
from authentik.lib.utils.reflection import get_env
from authentik.outposts.apps import MANAGED_OUTPOST
@ -33,7 +32,7 @@ class RuntimeDict(TypedDict):
platform: str
uname: str
openssl_version: str
openssl_fips_enabled: bool | None
openssl_fips_mode: bool
authentik_version: str
@ -72,9 +71,7 @@ class SystemInfoSerializer(PassiveSerializer):
"architecture": platform.machine(),
"authentik_version": get_full_version(),
"environment": get_env(),
"openssl_fips_enabled": (
backend._fips_enabled if LicenseKey.get_total().is_valid() else None
),
"openssl_fips_enabled": backend._fips_enabled,
"openssl_version": OPENSSL_VERSION,
"platform": platform.platform(),
"python_version": python_version,

View File

@ -1,13 +1,13 @@
{% extends "base/skeleton.html" %}
{% load authentik_core %}
{% load static %}
{% block title %}
API Browser - {{ brand.branding_title }}
{% endblock %}
{% block head %}
{% versioned_script "dist/standalone/api-browser/index-%v.js" %}
<script src="{% static 'dist/standalone/api-browser/index.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#151515" media="(prefers-color-scheme: dark)">
{% endblock %}

View File

@ -11,20 +11,21 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import SecretKeyFilter
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.tenants.utils import get_current_tenant
class FooterLinkSerializer(PassiveSerializer):
"""Links returned in Config API"""
href = CharField(read_only=True, allow_null=True)
href = CharField(read_only=True)
name = CharField(read_only=True)

View File

@ -17,6 +17,7 @@ from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodFiel
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
@ -25,7 +26,6 @@ from authentik.api.pagination import Pagination
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Application, User
from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.events.models import EventAction
@ -103,7 +103,7 @@ class ApplicationSerializer(ModelSerializer):
class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Application Viewset"""
queryset = Application.objects.all().prefetch_related("provider").prefetch_related("policies")
queryset = Application.objects.all().prefetch_related("provider")
serializer_class = ApplicationSerializer
search_fields = [
"name",

View File

@ -8,12 +8,12 @@ from rest_framework import mixins
from rest_framework.fields import SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from ua_parser import user_agent_parser
from authentik.api.authorization import OwnerSuperuserPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import AuthenticatedSession
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR, ASNDict
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR, GeoIPDict

View File

@ -17,12 +17,12 @@ from rest_framework.decorators import action
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ListSerializer, ValidationError
from rest_framework.serializers import ListSerializer, ModelSerializer, ValidationError
from rest_framework.validators import UniqueValidator
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Group, User
from authentik.rbac.api.roles import RoleSerializer
from authentik.rbac.decorators import permission_required

View File

@ -8,10 +8,11 @@ from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.fields import BooleanField, CharField, SerializerMethodField
from rest_framework.fields import BooleanField, CharField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.blueprints.api import ManagedSerializer
@ -19,7 +20,6 @@ from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
MetaNameSerializer,
ModelSerializer,
PassiveSerializer,
)
from authentik.core.expression.evaluator import PropertyMappingEvaluator

View File

@ -6,12 +6,13 @@ from django.utils.translation import gettext_lazy as _
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework import mixins
from rest_framework.fields import ReadOnlyField, SerializerMethodField
from rest_framework.fields import ReadOnlyField
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Provider

View File

@ -11,6 +11,7 @@ from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
@ -18,7 +19,7 @@ from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.file import (

View File

@ -12,6 +12,7 @@ from rest_framework.fields import CharField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.api.authorization import OwnerSuperuserPermissions
@ -19,7 +20,7 @@ from authentik.blueprints.api import ManagedSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import (
USER_ATTRIBUTE_TOKEN_EXPIRING,
USER_ATTRIBUTE_TOKEN_MAXIMUM_LIFETIME,
@ -44,13 +45,6 @@ class TokenSerializer(ManagedSerializer, ModelSerializer):
if SERIALIZER_CONTEXT_BLUEPRINT in self.context:
self.fields["key"] = CharField(required=False)
def validate_user(self, user: User):
"""Ensure user of token cannot be changed"""
if self.instance and self.instance.user_id:
if user.pk != self.instance.user_id:
raise ValidationError("User cannot be changed")
return user
def validate(self, attrs: dict[Any, str]) -> dict[Any, str]:
"""Ensure only API or App password tokens are created."""
request: Request = self.context.get("request")

View File

@ -40,6 +40,7 @@ from rest_framework.serializers import (
BooleanField,
DateTimeField,
ListSerializer,
ModelSerializer,
PrimaryKeyRelatedField,
ValidationError,
)
@ -51,12 +52,7 @@ from authentik.admin.api.metrics import CoordinateSerializer
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT
from authentik.brands.models import Brand
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
JSONDictField,
LinkSerializer,
ModelSerializer,
PassiveSerializer,
)
from authentik.core.api.utils import JSONDictField, LinkSerializer, PassiveSerializer
from authentik.core.middleware import (
SESSION_KEY_IMPERSONATE_ORIGINAL_USER,
SESSION_KEY_IMPERSONATE_USER,

View File

@ -12,12 +12,9 @@ from rest_framework.fields import (
JSONField,
SerializerMethodField,
)
from rest_framework.serializers import ModelSerializer as BaseModelSerializer
from rest_framework.serializers import (
Serializer,
ValidationError,
model_meta,
raise_errors_on_nested_writes,
)
@ -28,39 +25,6 @@ def is_dict(value: Any):
raise ValidationError("Value must be a dictionary, and not have any duplicate keys.")
class ModelSerializer(BaseModelSerializer):
def update(self, instance: Model, validated_data):
raise_errors_on_nested_writes("update", self, validated_data)
info = model_meta.get_field_info(instance)
# Simply set each attribute on the instance, and then save it.
# Note that unlike `.create()` we don't need to treat many-to-many
# relationships as being a special case. During updates we already
# have an instance pk for the relationships to be associated with.
m2m_fields = []
for attr, value in validated_data.items():
if attr in info.relations and info.relations[attr].to_many:
m2m_fields.append((attr, value))
else:
setattr(instance, attr, value)
instance.save()
# Note that many-to-many fields are set after updating instance.
# Setting m2m fields triggers signals which could potentially change
# updated instance and we do not want it to collide with .update()
for attr, value in m2m_fields:
field = getattr(instance, attr)
# We can't check for inheritance here as m2m managers are generated dynamically
if field.__class__.__name__ == "RelatedManager":
field.set(value, bulk=False)
else:
field.set(value)
return instance
class JSONDictField(JSONField):
"""JSON Field which only allows dictionaries"""

View File

@ -76,11 +76,8 @@ class PropertyMappingEvaluator(BaseEvaluator):
)
if "request" in self._context:
req: PolicyRequest = self._context["request"]
if req.http_request:
event.from_http(req.http_request, req.user)
return
elif req.user:
event.set_user(req.user)
event.from_http(req.http_request, req.user)
return
event.save()
def evaluate(self, *args, **kwargs) -> Any:

View File

@ -1,6 +1,5 @@
"""authentik core exceptions"""
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sentry import SentryIgnoredException
@ -13,7 +12,7 @@ class PropertyMappingExpressionException(SentryIgnoredException):
self.mapping = mapping
class SkipObjectException(ControlFlowException):
class SkipObjectException(PropertyMappingExpressionException):
"""Exception which can be raised in a property mapping to skip syncing an object.
Only applies to Property mappings which sync objects, and not on mappings which transitively
apply to a single user"""

View File

@ -26,7 +26,6 @@ from authentik.blueprints.models import ManagedModel
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.lib.avatars import get_avatar
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.generators import generate_id
from authentik.lib.models import (
CreatedUpdatedModel,
@ -784,8 +783,6 @@ class PropertyMapping(SerializerModel, ManagedModel):
evaluator = PropertyMappingEvaluator(self, user, request, **kwargs)
try:
return evaluator.evaluate(self.expression)
except ControlFlowException as exc:
raise exc
except Exception as exc:
raise PropertyMappingExpressionException(self, exc) from exc

View File

@ -309,7 +309,7 @@ class SourceFlowManager:
# When request isn't authenticated we jump straight to auth
if not self.request.user.is_authenticated:
return self.handle_auth(connection)
connection.save()
# Connection has already been saved
Event.new(
EventAction.SOURCE_LINKED,
message="Linked Source",

View File

@ -10,7 +10,7 @@
versionSubdomain: "{{ version_subdomain }}",
build: "{{ build }}",
};
window.addEventListener("DOMContentLoaded", function () {
window.addEventListener("DOMContentLoaded", () => {
{% for message in messages %}
window.dispatchEvent(
new CustomEvent("ak-message", {

View File

@ -1,6 +1,5 @@
{% load static %}
{% load i18n %}
{% load authentik_core %}
<!DOCTYPE html>
@ -15,8 +14,8 @@
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}">
<link rel="stylesheet" type="text/css" href="{% static 'dist/custom.css' %}" data-inject>
{% versioned_script "dist/poly-%v.js" %}
{% versioned_script "dist/standalone/loading/index-%v.js" %}
<script src="{% static 'dist/poly.js' %}?version={{ version }}" type="module"></script>
<script src="{% static 'dist/standalone/loading/index.js' %}?version={{ version }}" type="module"></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />

View File

@ -1,9 +1,9 @@
{% extends "base/skeleton.html" %}
{% load authentik_core %}
{% load static %}
{% block head %}
{% versioned_script "dist/admin/AdminInterface-%v.js" %}
<script src="{% static 'dist/admin/AdminInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
{% include "base/header_js.html" %}

View File

@ -1,7 +1,6 @@
{% extends "base/skeleton.html" %}
{% load static %}
{% load authentik_core %}
{% block head_before %}
{{ block.super }}
@ -18,7 +17,7 @@ window.authentik.flow = {
{% endblock %}
{% block head %}
{% versioned_script "dist/flow/FlowInterface-%v.js" %}
<script src="{% static 'dist/flow/FlowInterface.js' %}?version={{ version }}" type="module"></script>
<style>
:root {
--ak-flow-background: url("{{ flow.background_url }}");

View File

@ -1,9 +1,9 @@
{% extends "base/skeleton.html" %}
{% load authentik_core %}
{% load static %}
{% block head %}
{% versioned_script "dist/user/UserInterface-%v.js" %}
<script src="{% static 'dist/user/UserInterface.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#1c1e21" media="(prefers-color-scheme: dark)">
{% include "base/header_js.html" %}

View File

@ -71,9 +71,9 @@
</li>
{% endfor %}
<li>
<span>
<a href="https://goauthentik.io?utm_source=authentik">
{% trans 'Powered by authentik' %}
</span>
</a>
</li>
</ul>
</footer>

View File

@ -1,21 +0,0 @@
"""authentik core tags"""
from django import template
from django.templatetags.static import static as static_loader
from django.utils.safestring import mark_safe
from authentik import get_full_version
register = template.Library()
@register.simple_tag()
def versioned_script(path: str) -> str:
"""Wrapper around {% static %} tag that supports setting the version"""
returned_lines = [
(
f'<script src="{static_loader(path.replace("%v", get_full_version()))}'
'" type="module"></script>'
),
]
return mark_safe("".join(returned_lines)) # nosec

View File

@ -3,10 +3,7 @@
from django.test import RequestFactory, TestCase
from guardian.shortcuts import get_anonymous_user
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import PropertyMapping
from authentik.core.tests.utils import create_test_admin_user
from authentik.events.models import Event, EventAction
@ -45,17 +42,6 @@ class TestPropertyMappings(TestCase):
self.assertTrue(events.exists())
self.assertEqual(len(events), 1)
def test_expression_skip(self):
"""Test expression error"""
expr = "raise SkipObject"
mapping = PropertyMapping.objects.create(name=generate_id(), expression=expr)
with self.assertRaises(SkipObjectException):
mapping.evaluate(None, None)
events = Event.objects.filter(
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
)
self.assertFalse(events.exists())
def test_expression_error_extended(self):
"""Test expression error (with user and http request"""
expr = "return aaa"

View File

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

View File

@ -20,9 +20,8 @@ from authentik.core.api.transactional_applications import TransactionalApplicati
from authentik.core.api.users import UserViewSet
from authentik.core.views import apps
from authentik.core.views.debug import AccessDeniedView
from authentik.core.views.interface import InterfaceView
from authentik.core.views.interface import FlowInterfaceView, InterfaceView
from authentik.core.views.session import EndSessionView
from authentik.flows.views.interface import FlowInterfaceView
from authentik.root.asgi_middleware import SessionMiddleware
from authentik.root.messages.consumer import MessageConsumer
from authentik.root.middleware import ChannelsLoggingMiddleware
@ -54,8 +53,6 @@ urlpatterns = [
),
path(
"if/flow/<slug:flow_slug>/",
# FIXME: move this url to the flows app...also will cause all
# of the reverse calls to be adjusted
ensure_csrf_cookie(FlowInterfaceView.as_view()),
name="if-flow",
),

View File

@ -3,6 +3,7 @@
from json import dumps
from typing import Any
from django.shortcuts import get_object_or_404
from django.views.generic.base import TemplateView
from rest_framework.request import Request
@ -10,6 +11,7 @@ from authentik import get_build_hash
from authentik.admin.tasks import LOCAL_VERSION
from authentik.api.v3.config import ConfigView
from authentik.brands.api import CurrentBrandSerializer
from authentik.flows.models import Flow
class InterfaceView(TemplateView):
@ -23,3 +25,14 @@ class InterfaceView(TemplateView):
kwargs["build"] = get_build_hash()
kwargs["url_kwargs"] = self.kwargs
return super().get_context_data(**kwargs)
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
template_name = "if/flow.html"
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)

View File

@ -24,12 +24,13 @@ from rest_framework.fields import (
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import SecretKeyFilter
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.crypto.apps import MANAGED_KEY
from authentik.crypto.builder import CertificateBuilder, PrivateKeyAlg
from authentik.crypto.models import CertificateKeyPair

View File

@ -13,10 +13,11 @@ from rest_framework.fields import CharField, IntegerField
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import User, UserTypes
from authentik.enterprise.license import LicenseKey, LicenseSummarySerializer
from authentik.enterprise.models import License

View File

@ -1,13 +1,12 @@
"""GoogleWorkspaceProviderGroup API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
@ -31,7 +30,6 @@ class GoogleWorkspaceProviderGroupSerializer(ModelSerializer):
class GoogleWorkspaceProviderGroupViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -1,13 +1,12 @@
"""GoogleWorkspaceProviderUser API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.google_workspace.models import GoogleWorkspaceProviderUser
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
@ -31,7 +30,6 @@ class GoogleWorkspaceProviderUserSerializer(ModelSerializer):
class GoogleWorkspaceProviderUserViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -214,7 +214,3 @@ class GoogleWorkspaceGroupClient(
google_id=google_id,
attributes=group,
)
def update_single_attribute(self, connection: GoogleWorkspaceProviderUser):
group = self.directory_service.groups().get(connection.google_id)
connection.attributes = group

View File

@ -119,7 +119,3 @@ class GoogleWorkspaceUserClient(GoogleWorkspaceSyncClient[User, GoogleWorkspaceP
google_id=email,
attributes=user,
)
def update_single_attribute(self, connection: GoogleWorkspaceProviderUser):
user = self.directory_service.users().get(connection.google_id)
connection.attributes = user

View File

@ -31,58 +31,6 @@ def default_scopes() -> list[str]:
]
class GoogleWorkspaceProviderUser(SerializerModel):
"""Mapping of a user and provider to a Google user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey("GoogleWorkspaceProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.users import (
GoogleWorkspaceProviderUserSerializer,
)
return GoogleWorkspaceProviderUserSerializer
class Meta:
verbose_name = _("Google Workspace Provider User")
verbose_name_plural = _("Google Workspace Provider Users")
unique_together = (("google_id", "user", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Google group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey("GoogleWorkspaceProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.groups import (
GoogleWorkspaceProviderGroupSerializer,
)
return GoogleWorkspaceProviderGroupSerializer
class Meta:
verbose_name = _("Google Workspace Provider Group")
verbose_name_plural = _("Google Workspace Provider Groups")
unique_together = (("google_id", "group", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}"
class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
"""Sync users from authentik into Google Workspace."""
@ -111,16 +59,15 @@ class GoogleWorkspaceProvider(OutgoingSyncProvider, BackchannelProvider):
)
def client_for_model(
self,
model: type[User | Group | GoogleWorkspaceProviderUser | GoogleWorkspaceProviderGroup],
self, model: type[User | Group]
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User | GoogleWorkspaceProviderUser):
if issubclass(model, User):
from authentik.enterprise.providers.google_workspace.clients.users import (
GoogleWorkspaceUserClient,
)
return GoogleWorkspaceUserClient(self)
if issubclass(model, Group | GoogleWorkspaceProviderGroup):
if issubclass(model, Group):
from authentik.enterprise.providers.google_workspace.clients.groups import (
GoogleWorkspaceGroupClient,
)
@ -197,3 +144,55 @@ class GoogleWorkspaceProviderMapping(PropertyMapping):
class Meta:
verbose_name = _("Google Workspace Provider Mapping")
verbose_name_plural = _("Google Workspace Provider Mappings")
class GoogleWorkspaceProviderUser(SerializerModel):
"""Mapping of a user and provider to a Google user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.users import (
GoogleWorkspaceProviderUserSerializer,
)
return GoogleWorkspaceProviderUserSerializer
class Meta:
verbose_name = _("Google Workspace Provider User")
verbose_name_plural = _("Google Workspace Provider Users")
unique_together = (("google_id", "user", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider User {self.user_id} to {self.provider_id}"
class GoogleWorkspaceProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Google group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
google_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(GoogleWorkspaceProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.google_workspace.api.groups import (
GoogleWorkspaceProviderGroupSerializer,
)
return GoogleWorkspaceProviderGroupSerializer
class Meta:
verbose_name = _("Google Workspace Provider Group")
verbose_name_plural = _("Google Workspace Provider Groups")
unique_together = (("google_id", "group", "provider"),)
def __str__(self) -> str:
return f"Google Workspace Provider Group {self.group_id} to {self.provider_id}"

View File

@ -1,13 +1,12 @@
"""MicrosoftEntraProviderGroup API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderGroup
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
@ -31,7 +30,6 @@ class MicrosoftEntraProviderGroupSerializer(ModelSerializer):
class MicrosoftEntraProviderGroupViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

View File

@ -1,13 +1,12 @@
"""MicrosoftEntraProviderUser API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.providers.microsoft_entra.models import MicrosoftEntraProviderUser
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
class MicrosoftEntraProviderUserSerializer(ModelSerializer):
@ -30,7 +29,6 @@ class MicrosoftEntraProviderUserSerializer(ModelSerializer):
class MicrosoftEntraProviderUserViewSet(
OutgoingSyncConnectionCreateMixin,
mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,

View File

@ -226,7 +226,3 @@ class MicrosoftEntraGroupClient(
microsoft_id=group.id,
attributes=self.entity_as_dict(group),
)
def update_single_attribute(self, connection: MicrosoftEntraProviderGroup):
data = self._request(self.client.groups.by_group_id(connection.microsoft_id).get())
connection.attributes = self.entity_as_dict(data)

View File

@ -66,26 +66,6 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
microsoft_user.delete()
return response
def get_select_fields(self) -> list[str]:
"""All fields that should be selected when we fetch user data."""
# TODO: Make this customizable in the future
return [
# Default fields
"businessPhones",
"displayName",
"givenName",
"jobTitle",
"mail",
"mobilePhone",
"officeLocation",
"preferredLanguage",
"surname",
"userPrincipalName",
"id",
# Required for logging into M365 using authentik
"onPremisesImmutableId",
]
def create(self, user: User):
"""Create user from scratch and create a connection object"""
microsoft_user = self.to_schema(user, None)
@ -95,12 +75,12 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
response = self._request(self.client.users.post(microsoft_user))
except ObjectExistsSyncException:
# user already exists in microsoft entra, so we can connect them manually
query_params = UsersRequestBuilder.UsersRequestBuilderGetQueryParameters()(
filter=f"mail eq '{microsoft_user.mail}'",
)
request_configuration = (
UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
filter=f"mail eq '{microsoft_user.mail}'",
select=self.get_select_fields(),
),
query_parameters=query_params,
)
)
user_data = self._request(self.client.users.get(request_configuration))
@ -119,6 +99,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
except TransientSyncException as exc:
raise exc
else:
print(self.entity_as_dict(response))
return MicrosoftEntraProviderUser.objects.create(
provider=self.provider,
user=user,
@ -139,12 +120,7 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
def discover(self):
"""Iterate through all users and connect them with authentik users if possible"""
request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=self.get_select_fields(),
),
)
users = self._request(self.client.users.get(request_configuration))
users = self._request(self.client.users.get())
next_link = True
while next_link:
for user in users.value:
@ -165,14 +141,3 @@ class MicrosoftEntraUserClient(MicrosoftEntraSyncClient[User, MicrosoftEntraProv
microsoft_id=user.id,
attributes=self.entity_as_dict(user),
)
def update_single_attribute(self, connection: MicrosoftEntraProviderUser):
request_configuration = UsersRequestBuilder.UsersRequestBuilderGetRequestConfiguration(
query_parameters=UsersRequestBuilder.UsersRequestBuilderGetQueryParameters(
select=self.get_select_fields(),
),
)
data = self._request(
self.client.users.by_user_id(connection.microsoft_id).get(request_configuration)
)
connection.attributes = self.entity_as_dict(data)

View File

@ -22,58 +22,6 @@ from authentik.lib.sync.outgoing.base import BaseOutgoingSyncClient
from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction, OutgoingSyncProvider
class MicrosoftEntraProviderUser(SerializerModel):
"""Mapping of a user and provider to a Microsoft user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey("MicrosoftEntraProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.users import (
MicrosoftEntraProviderUserSerializer,
)
return MicrosoftEntraProviderUserSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider User")
verbose_name_plural = _("Microsoft Entra Provider User")
unique_together = (("microsoft_id", "user", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}"
class MicrosoftEntraProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Microsoft group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey("MicrosoftEntraProvider", on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.groups import (
MicrosoftEntraProviderGroupSerializer,
)
return MicrosoftEntraProviderGroupSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider Group")
verbose_name_plural = _("Microsoft Entra Provider Groups")
unique_together = (("microsoft_id", "group", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}"
class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
"""Sync users from authentik into Microsoft Entra."""
@ -100,16 +48,15 @@ class MicrosoftEntraProvider(OutgoingSyncProvider, BackchannelProvider):
)
def client_for_model(
self,
model: type[User | Group | MicrosoftEntraProviderUser | MicrosoftEntraProviderGroup],
self, model: type[User | Group]
) -> BaseOutgoingSyncClient[User | Group, Any, Any, Self]:
if issubclass(model, User | MicrosoftEntraProviderUser):
if issubclass(model, User):
from authentik.enterprise.providers.microsoft_entra.clients.users import (
MicrosoftEntraUserClient,
)
return MicrosoftEntraUserClient(self)
if issubclass(model, Group | MicrosoftEntraProviderGroup):
if issubclass(model, Group):
from authentik.enterprise.providers.microsoft_entra.clients.groups import (
MicrosoftEntraGroupClient,
)
@ -186,3 +133,55 @@ class MicrosoftEntraProviderMapping(PropertyMapping):
class Meta:
verbose_name = _("Microsoft Entra Provider Mapping")
verbose_name_plural = _("Microsoft Entra Provider Mappings")
class MicrosoftEntraProviderUser(SerializerModel):
"""Mapping of a user and provider to a Microsoft user ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
user = models.ForeignKey(User, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.users import (
MicrosoftEntraProviderUserSerializer,
)
return MicrosoftEntraProviderUserSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider User")
verbose_name_plural = _("Microsoft Entra Provider User")
unique_together = (("microsoft_id", "user", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider User {self.user_id} to {self.provider_id}"
class MicrosoftEntraProviderGroup(SerializerModel):
"""Mapping of a group and provider to a Microsoft group ID"""
id = models.UUIDField(primary_key=True, editable=False, default=uuid4)
microsoft_id = models.TextField()
group = models.ForeignKey(Group, on_delete=models.CASCADE)
provider = models.ForeignKey(MicrosoftEntraProvider, on_delete=models.CASCADE)
attributes = models.JSONField(default=dict)
@property
def serializer(self) -> type[Serializer]:
from authentik.enterprise.providers.microsoft_entra.api.groups import (
MicrosoftEntraProviderGroupSerializer,
)
return MicrosoftEntraProviderGroupSerializer
class Meta:
verbose_name = _("Microsoft Entra Provider Group")
verbose_name_plural = _("Microsoft Entra Provider Groups")
unique_together = (("microsoft_id", "group", "provider"),)
def __str__(self) -> str:
return f"Microsoft Entra Provider Group {self.group_id} to {self.provider_id}"

View File

@ -3,18 +3,16 @@
from unittest.mock import AsyncMock, MagicMock, patch
from azure.identity.aio import ClientSecretCredential
from django.urls import reverse
from django.test import TestCase
from msgraph.generated.models.group_collection_response import GroupCollectionResponse
from msgraph.generated.models.organization import Organization
from msgraph.generated.models.organization_collection_response import OrganizationCollectionResponse
from msgraph.generated.models.user import User as MSUser
from msgraph.generated.models.user_collection_response import UserCollectionResponse
from msgraph.generated.models.verified_domain import VerifiedDomain
from rest_framework.test import APITestCase
from authentik.blueprints.tests import apply_blueprint
from authentik.core.models import Application, Group, User
from authentik.core.tests.utils import create_test_admin_user
from authentik.enterprise.providers.microsoft_entra.models import (
MicrosoftEntraProvider,
MicrosoftEntraProviderMapping,
@ -27,12 +25,11 @@ from authentik.lib.sync.outgoing.models import OutgoingSyncDeleteAction
from authentik.tenants.models import Tenant
class MicrosoftEntraUserTests(APITestCase):
class MicrosoftEntraUserTests(TestCase):
"""Microsoft Entra User tests"""
@apply_blueprint("system/providers-microsoft-entra.yaml")
def setUp(self) -> None:
# Delete all users and groups as the mocked HTTP responses only return one ID
# which will cause errors with multiple users
Tenant.objects.update(avatars="none")
@ -374,45 +371,3 @@ class MicrosoftEntraUserTests(APITestCase):
)
self.assertFalse(Event.objects.filter(action=EventAction.SYSTEM_EXCEPTION).exists())
user_list.assert_called_once()
def test_connect_manual(self):
"""test manual user connection"""
uid = generate_id()
self.app.backchannel_providers.remove(self.provider)
admin = create_test_admin_user()
different_user = User.objects.create(
username=uid,
email=f"{uid}@goauthentik.io",
)
self.app.backchannel_providers.add(self.provider)
with (
patch(
"authentik.enterprise.providers.microsoft_entra.models.MicrosoftEntraProvider.microsoft_credentials",
MagicMock(return_value={"credentials": self.creds}),
),
patch(
"msgraph.generated.organization.organization_request_builder.OrganizationRequestBuilder.get",
AsyncMock(
return_value=OrganizationCollectionResponse(
value=[
Organization(verified_domains=[VerifiedDomain(name="goauthentik.io")])
]
)
),
),
patch(
"authentik.enterprise.providers.microsoft_entra.clients.users.MicrosoftEntraUserClient.update_single_attribute",
MagicMock(),
) as user_get,
):
self.client.force_login(admin)
response = self.client.post(
reverse("authentik_api:microsoftentraprovideruser-list"),
data={
"microsoft_id": generate_id(),
"user": different_user.pk,
"provider": self.provider.pk,
},
)
self.assertEqual(response.status_code, 201)
user_get.assert_called_once()

View File

@ -3,12 +3,12 @@
from django_filters.rest_framework.backends import DjangoFilterBackend
from rest_framework import mixins
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerSuperuserPermissions
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.endpoints import EndpointSerializer
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer

View File

@ -8,11 +8,11 @@ from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_sche
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.core.models import Provider
from authentik.enterprise.api import EnterpriseRequiredMixin
from authentik.enterprise.providers.rac.api.providers import RACProviderSerializer

View File

@ -1,9 +1,9 @@
{% extends "base/skeleton.html" %}
{% load authentik_core %}
{% load static %}
{% block head %}
{% versioned_script "dist/enterprise/rac/index-%v.js" %}
<script src="{% static 'dist/enterprise/rac/index.js' %}?version={{ version }}" type="module"></script>
<meta name="theme-color" content="#18191a" media="(prefers-color-scheme: dark)">
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
<link rel="icon" href="{{ tenant.branding_favicon }}">

View File

@ -15,11 +15,12 @@ from rest_framework.decorators import action
from rest_framework.fields import DictField, IntegerField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.object_types import TypeCreateSerializer
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction

View File

@ -1,9 +1,9 @@
"""NotificationWebhookMapping API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.models import NotificationWebhookMapping

View File

@ -1,10 +1,10 @@
"""NotificationRule API Views"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.models import NotificationRule

View File

@ -9,10 +9,11 @@ from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import (
Event,
Notification,

View File

@ -9,11 +9,11 @@ from rest_framework.fields import ReadOnlyField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.events.api.events import EventSerializer
from authentik.events.models import Notification

View File

@ -16,10 +16,10 @@ from rest_framework.fields import (
)
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ReadOnlyModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.utils import ModelSerializer
from authentik.events.logs import LogEventSerializer
from authentik.events.models import SystemTask, TaskStatus
from authentik.rbac.decorators import permission_required

View File

@ -75,10 +75,7 @@ def on_login_failed(
**kwargs,
):
"""Failed Login, authentik custom event"""
user = User.objects.filter(username=credentials.get("username")).first()
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(
request, user
)
Event.new(EventAction.LOGIN_FAILED, **credentials, stage=stage, **kwargs).from_http(request)
@receiver(invitation_used)

View File

@ -3,10 +3,10 @@
from typing import Any
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.flows.api.stages import StageSerializer
from authentik.flows.models import FlowStageBinding

View File

@ -7,22 +7,18 @@ from django.utils.translation import gettext as _
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiResponse, extend_schema
from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, ReadOnlyField, SerializerMethodField
from rest_framework.fields import BooleanField, CharField, ReadOnlyField
from rest_framework.parsers import MultiPartParser
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.blueprints.v1.exporter import FlowExporter
from authentik.blueprints.v1.importer import SERIALIZER_CONTEXT_BLUEPRINT, Importer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
CacheSerializer,
LinkSerializer,
ModelSerializer,
PassiveSerializer,
)
from authentik.core.api.utils import CacheSerializer, LinkSerializer, PassiveSerializer
from authentik.events.logs import LogEventSerializer
from authentik.flows.api.flows_diagram import FlowDiagram, FlowDiagramSerializer
from authentik.flows.exceptions import FlowNonApplicableException

View File

@ -4,15 +4,15 @@ from django.urls.base import reverse
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSetSerializer
from authentik.flows.models import ConfigurableStage, Stage

View File

@ -1,54 +0,0 @@
{% load static %}
{% load i18n %}
{% load authentik_core %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>{% block title %}{% trans title|default:brand.branding_title %}{% endblock %}</title>
<link rel="icon" href="{{ brand.branding_favicon }}">
<link rel="shortcut icon" href="{{ brand.branding_favicon }}">
{% block head_before %}
{% endblock %}
<link rel="stylesheet" type="text/css" href="{% static 'dist/sfe/bootstrap.min.css' %}">
<meta name="sentry-trace" content="{{ sentry_trace }}" />
{% include "base/header_js.html" %}
<style>
html,
body {
height: 100%;
}
body {
background-image: url("{{ flow.background_url }}");
background-repeat: no-repeat;
background-size: cover;
}
.card {
padding: 3rem;
}
.form-signin {
max-width: 330px;
padding: 1rem;
}
.form-signin .form-floating:focus-within {
z-index: 2;
}
.brand-icon {
max-width: 100%;
}
</style>
</head>
<body class="d-flex align-items-center py-4 bg-body-tertiary">
<div class="card m-auto">
<main class="form-signin w-100 m-auto" id="flow-sfe-container">
</main>
<span class="mt-3 mb-0 text-muted text-center">{% trans 'Powered by authentik' %}</span>
</div>
<script src="{% static 'dist/sfe/index.js' %}"></script>
</body>
</html>

View File

@ -16,7 +16,6 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
from django.views.generic import View
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, PolymorphicProxySerializer, extend_schema
from rest_framework.authentication import SessionAuthentication
from rest_framework.permissions import AllowAny
from rest_framework.views import APIView
from sentry_sdk import capture_exception
@ -24,7 +23,6 @@ from sentry_sdk.api import set_tag
from sentry_sdk.hub import Hub
from structlog.stdlib import BoundLogger, get_logger
from authentik.api.authentication import TokenAuthentication
from authentik.brands.models import Brand
from authentik.core.models import Application
from authentik.events.models import Event, EventAction, cleanse_dict
@ -106,10 +104,6 @@ class FlowExecutorView(APIView):
"""Flow executor, passing requests to Stage Views"""
permission_classes = [AllowAny]
authentication_classes = [
TokenAuthentication,
SessionAuthentication,
]
flow: Flow

View File

@ -1,41 +0,0 @@
"""Interface views"""
from typing import Any
from django.shortcuts import get_object_or_404
from ua_parser.user_agent_parser import Parse
from authentik.core.views.interface import InterfaceView
from authentik.flows.models import Flow
class FlowInterfaceView(InterfaceView):
"""Flow interface"""
def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
kwargs["flow"] = get_object_or_404(Flow, slug=self.kwargs.get("flow_slug"))
kwargs["inspector"] = "inspector" in self.request.GET
return super().get_context_data(**kwargs)
def compat_needs_sfe(self) -> bool:
"""Check if we need to use the simplified flow executor for compatibility"""
ua = Parse(self.request.META.get("HTTP_USER_AGENT", ""))
if ua["user_agent"]["family"] == "IE":
return True
# Only use SFE for Edge 18 and older, after Edge 18 MS switched to chromium which supports
# the default flow executor
if (
ua["user_agent"]["family"] == "Edge"
and int(ua["user_agent"]["major"]) <= 18 # noqa: PLR2004
): # noqa: PLR2004
return True
# https://github.com/AzureAD/microsoft-authentication-library-for-objc
# Used by Microsoft Teams/Office on macOS, and also uses a very outdated browser engine
if "PKeyAuth" in ua["string"]:
return True
return False
def get_template_names(self) -> list[str]:
if self.compat_needs_sfe() or "sfe" in self.request.GET:
return ["if/flow-sfe.html"]
return ["if/flow.html"]

View File

@ -50,6 +50,7 @@ cache:
timeout: 300
timeout_flows: 300
timeout_policies: 300
timeout_reputation: 300
# channel:
# url: ""
@ -115,9 +116,6 @@ events:
context_processors:
geoip: "/geoip/GeoLite2-City.mmdb"
asn: "/geoip/GeoLite2-ASN.mmdb"
compliance:
fips:
enabled: false
cert_discovery_dir: /certs

View File

@ -19,7 +19,6 @@ from structlog.stdlib import get_logger
from authentik.core.models import User
from authentik.events.models import Event
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.utils.http import get_http_session
from authentik.policies.models import Policy, PolicyBinding
from authentik.policies.process import PolicyProcess
@ -217,8 +216,7 @@ class BaseEvaluator:
# so the user only sees information relevant to them
# and none of our surrounding error handling
exc.__traceback__ = exc.__traceback__.tb_next
if not isinstance(exc, ControlFlowException):
self.handle_error(exc, expression_source)
self.handle_error(exc, expression_source)
raise exc
return result

View File

@ -1,6 +0,0 @@
from authentik.lib.sentry import SentryIgnoredException
class ControlFlowException(SentryIgnoredException):
"""Exceptions used to control the flow from exceptions, not reported as a warning/
error in logs"""

View File

@ -62,7 +62,7 @@ class DomainlessURLValidator(URLValidator):
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{1,5})?" # port
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,
@ -88,7 +88,7 @@ class DomainlessFormattedURLValidator(DomainlessURLValidator):
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
r"(?:" + self.ipv4_re + "|" + self.ipv6_re + "|" + self.host_re + ")"
r"(?::\d{1,5})?" # port
r"(?::\d{2,5})?" # port
r"(?:[/?#][^\s]*)?" # resource path
r"\Z",
re.IGNORECASE,

View File

@ -59,9 +59,8 @@ def sentry_init(**sentry_init_kwargs):
"_experiments": {
"profiles_sample_rate": float(CONFIG.get("error_reporting.sample_rate", 0.1)),
},
**sentry_init_kwargs,
**CONFIG.get_dict_from_b64_json("error_reporting.extra_args", {}),
}
kwargs.update(**sentry_init_kwargs)
sentry_sdk_init(
dsn=CONFIG.get("error_reporting.sentry_dsn"),

View File

@ -4,11 +4,8 @@ from django.db.models import QuerySet
from django.http import HttpRequest
from authentik.core.expression.evaluator import PropertyMappingEvaluator
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
)
from authentik.core.expression.exceptions import PropertyMappingExpressionException
from authentik.core.models import PropertyMapping, User
from authentik.lib.expression.exceptions import ControlFlowException
class PropertyMappingManager:
@ -60,7 +57,7 @@ class PropertyMappingManager:
mapping.set_context(user, request, **kwargs)
try:
value = mapping.evaluate(mapping.model.expression)
except (PropertyMappingExpressionException, ControlFlowException) as exc:
except PropertyMappingExpressionException as exc:
raise exc from exc
except Exception as exc:
raise PropertyMappingExpressionException(exc, mapping.model) from exc

View File

@ -8,7 +8,7 @@ from rest_framework.fields import BooleanField
from rest_framework.request import Request
from rest_framework.response import Response
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.events.api.tasks import SystemTaskSerializer
from authentik.lib.sync.outgoing.models import OutgoingSyncProvider
@ -54,17 +54,3 @@ class OutgoingSyncProviderStatusMixin:
"is_running": not lock_acquired,
}
return Response(SyncStatusSerializer(status).data)
class OutgoingSyncConnectionCreateMixin:
"""Mixin for connection objects that fetches remote data upon creation"""
def perform_create(self, serializer: ModelSerializer):
super().perform_create(serializer)
try:
instance = serializer.instance
client = instance.provider.client_for_model(instance.__class__)
client.update_single_attribute(instance)
instance.save()
except NotImplementedError:
pass

View File

@ -9,9 +9,9 @@ from structlog.stdlib import get_logger
from authentik.core.expression.exceptions import (
PropertyMappingExpressionException,
SkipObjectException,
)
from authentik.events.models import Event, EventAction
from authentik.lib.expression.exceptions import ControlFlowException
from authentik.lib.sync.mapper import PropertyMappingManager
from authentik.lib.sync.outgoing.exceptions import NotFoundSyncException, StopSync
from authentik.lib.utils.errors import exception_to_string
@ -92,7 +92,7 @@ class BaseOutgoingSyncClient[
eval_kwargs.setdefault("user", None)
for value in self.mapper.iter_eval(**eval_kwargs):
always_merger.merge(raw_final_object, value)
except ControlFlowException as exc:
except SkipObjectException as exc:
raise exc from exc
except PropertyMappingExpressionException as exc:
# Value error can be raised when assigning invalid data to an attribute
@ -114,8 +114,3 @@ class BaseOutgoingSyncClient[
pre-link any users/groups in the remote system with the respective
object in authentik based on a common identifier"""
raise NotImplementedError()
def update_single_attribute(self, connection: TConnection):
"""Update connection attributes on a connection object, when the connection
is manually created"""
raise NotImplementedError

View File

@ -6,21 +6,19 @@ from django_filters.filters import ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
from drf_spectacular.utils import extend_schema
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import BooleanField, CharField, DateTimeField, SerializerMethodField
from rest_framework.fields import BooleanField, CharField, DateTimeField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, ValidationError
from rest_framework.viewsets import ModelViewSet
from authentik import get_build_hash
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import JSONDictField, ModelSerializer, PassiveSerializer
from authentik.core.api.utils import JSONDictField, PassiveSerializer
from authentik.core.models import Provider
from authentik.enterprise.license import LicenseKey
from authentik.enterprise.providers.rac.models import RACProvider
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
from authentik.outposts.api.service_connections import ServiceConnectionSerializer
from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME
from authentik.outposts.models import (
@ -50,10 +48,6 @@ class OutpostSerializer(ModelSerializer):
service_connection_obj = ServiceConnectionSerializer(
source="service_connection", read_only=True
)
refresh_interval_s = SerializerMethodField()
def get_refresh_interval_s(self, obj: Outpost) -> int:
return int(timedelta_from_string(obj.config.refresh_interval).total_seconds())
def validate_name(self, name: str) -> str:
"""Validate name (especially for embedded outpost)"""
@ -89,8 +83,7 @@ class OutpostSerializer(ModelSerializer):
def validate_config(self, config) -> dict:
"""Check that the config has all required fields"""
try:
parsed = from_dict(OutpostConfig, config)
timedelta_string_validator(parsed.refresh_interval)
from_dict(OutpostConfig, config)
except DaciteError as exc:
raise ValidationError(f"Failed to validate config: {str(exc)}") from exc
return config
@ -105,7 +98,6 @@ class OutpostSerializer(ModelSerializer):
"providers_obj",
"service_connection",
"service_connection_obj",
"refresh_interval_s",
"token_identifier",
"config",
"managed",
@ -128,7 +120,7 @@ class OutpostHealthSerializer(PassiveSerializer):
golang_version = CharField(read_only=True)
openssl_enabled = BooleanField(read_only=True)
openssl_version = CharField(read_only=True)
fips_enabled = SerializerMethodField()
fips_enabled = BooleanField(read_only=True)
version_should = CharField(read_only=True)
version_outdated = BooleanField(read_only=True)
@ -138,12 +130,6 @@ class OutpostHealthSerializer(PassiveSerializer):
hostname = CharField(read_only=True, required=False)
def get_fips_enabled(self, obj: dict) -> bool | None:
"""Get FIPS enabled"""
if not LicenseKey.get_total().is_valid():
return None
return obj["fips_enabled"]
class OutpostFilter(FilterSet):
"""Filter for Outposts"""

View File

@ -12,13 +12,13 @@ from rest_framework.decorators import action
from rest_framework.fields import BooleanField, CharField, ReadOnlyField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.object_types import TypesMixin
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
MetaNameSerializer,
ModelSerializer,
PassiveSerializer,
)
from authentik.outposts.models import (

View File

@ -61,7 +61,6 @@ class OutpostConfig:
log_level: str = CONFIG.get("log_level")
object_naming_template: str = field(default="ak-outpost-%(name)s")
refresh_interval: str = "minutes=5"
container_image: str | None = field(default=None)

View File

@ -5,15 +5,13 @@ from collections import OrderedDict
from django.core.exceptions import ObjectDoesNotExist
from django_filters.filters import BooleanFilter, ModelMultipleChoiceFilter
from django_filters.filterset import FilterSet
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import PrimaryKeyRelatedField
from rest_framework.serializers import ModelSerializer, PrimaryKeyRelatedField, ValidationError
from rest_framework.viewsets import ModelViewSet
from structlog.stdlib import get_logger
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.models import PolicyBinding, PolicyBindingModel

View File

@ -6,9 +6,9 @@ from drf_spectacular.utils import OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework import mixins
from rest_framework.decorators import action
from rest_framework.fields import SerializerMethodField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
@ -18,7 +18,6 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import (
CacheSerializer,
MetaNameSerializer,
ModelSerializer,
)
from authentik.events.logs import LogEventSerializer, capture_logs
from authentik.policies.api.exec import PolicyTestResultSerializer, PolicyTestSerializer

View File

@ -1,22 +1,16 @@
"""Reputation policy API Views"""
from django.utils.translation import gettext_lazy as _
from django_filters.filters import BaseInFilter, CharFilter
from django_filters.filterset import FilterSet
from rest_framework import mixins
from rest_framework.exceptions import ValidationError
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet, ModelViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.policies.api.policies import PolicySerializer
from authentik.policies.reputation.models import Reputation, ReputationPolicy
class CharInFilter(BaseInFilter, CharFilter):
pass
class ReputationPolicySerializer(PolicySerializer):
"""Reputation Policy Serializer"""
@ -44,16 +38,6 @@ class ReputationPolicyViewSet(UsedByMixin, ModelViewSet):
ordering = ["name"]
class ReputationFilter(FilterSet):
"""Filter for reputation"""
identifier_in = CharInFilter(field_name="identifier", lookup_expr="in")
class Meta:
model = Reputation
fields = ["identifier", "ip", "score"]
class ReputationSerializer(ModelSerializer):
"""Reputation Serializer"""
@ -82,5 +66,5 @@ class ReputationViewSet(
queryset = Reputation.objects.all()
serializer_class = ReputationSerializer
search_fields = ["identifier", "ip", "score"]
filterset_class = ReputationFilter
filterset_fields = ["identifier", "ip", "score"]
ordering = ["ip"]

View File

@ -2,6 +2,8 @@
from authentik.blueprints.apps import ManagedAppConfig
CACHE_KEY_PREFIX = "goauthentik.io/policies/reputation/scores/"
class AuthentikPolicyReputationConfig(ManagedAppConfig):
"""Authentik reputation app config"""

View File

@ -1,25 +0,0 @@
# Generated by Django 5.0.6 on 2024-06-11 08:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_policies_reputation", "0006_reputation_ip_asn_data"),
]
operations = [
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["identifier"], name="authentik_p_identif_9434d7_idx"),
),
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["ip"], name="authentik_p_ip_7ad0df_idx"),
),
migrations.AddIndex(
model_name="reputation",
index=models.Index(fields=["ip", "identifier"], name="authentik_p_ip_d779aa_idx"),
),
]

View File

@ -96,8 +96,3 @@ class Reputation(ExpiringModel, SerializerModel):
verbose_name = _("Reputation Score")
verbose_name_plural = _("Reputation Scores")
unique_together = ("identifier", "ip")
indexes = [
models.Index(fields=["identifier"]),
models.Index(fields=["ip"]),
models.Index(fields=["ip", "identifier"]),
]

View File

@ -0,0 +1,11 @@
"""Reputation Settings"""
from celery.schedules import crontab
CELERY_BEAT_SCHEDULE = {
"policies_reputation_save": {
"task": "authentik.policies.reputation.tasks.save_reputation",
"schedule": crontab(minute="1-59/5"),
"options": {"queue": "authentik_scheduled"},
},
}

View File

@ -1,42 +1,40 @@
"""authentik reputation request signals"""
from django.contrib.auth.signals import user_logged_in
from django.db import transaction
from django.db.models import F
from django.core.cache import cache
from django.dispatch import receiver
from django.http import HttpRequest
from structlog.stdlib import get_logger
from authentik.core.signals import login_failed
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.policies.reputation.models import Reputation, reputation_expiry
from authentik.lib.config import CONFIG
from authentik.policies.reputation.apps import CACHE_KEY_PREFIX
from authentik.policies.reputation.tasks import save_reputation
from authentik.root.middleware import ClientIPMiddleware
from authentik.stages.identification.signals import identification_failed
LOGGER = get_logger()
CACHE_TIMEOUT = CONFIG.get_int("cache.timeout_reputation")
def update_score(request: HttpRequest, identifier: str, amount: int):
"""Update score for IP and User"""
remote_ip = ClientIPMiddleware.get_client_ip(request)
with transaction.atomic():
reputation, created = Reputation.objects.select_for_update().get_or_create(
ip=remote_ip,
identifier=identifier,
defaults={
"score": amount,
"ip_geo_data": GEOIP_CONTEXT_PROCESSOR.city_dict(remote_ip) or {},
"ip_asn_data": ASN_CONTEXT_PROCESSOR.asn_dict(remote_ip) or {},
"expires": reputation_expiry(),
},
try:
# We only update the cache here, as its faster than writing to the DB
score = cache.get_or_set(
CACHE_KEY_PREFIX + remote_ip + "/" + identifier,
{"ip": remote_ip, "identifier": identifier, "score": 0},
CACHE_TIMEOUT,
)
score["score"] += amount
cache.set(CACHE_KEY_PREFIX + remote_ip + "/" + identifier, score)
except ValueError as exc:
LOGGER.warning("failed to set reputation", exc=exc)
if not created:
reputation.score = F("score") + amount
reputation.save()
LOGGER.debug("Updated score", amount=amount, for_user=identifier, for_ip=remote_ip)
save_reputation.delay()
@receiver(login_failed)

View File

@ -0,0 +1,32 @@
"""Reputation tasks"""
from django.core.cache import cache
from structlog.stdlib import get_logger
from authentik.events.context_processors.asn import ASN_CONTEXT_PROCESSOR
from authentik.events.context_processors.geoip import GEOIP_CONTEXT_PROCESSOR
from authentik.events.models import TaskStatus
from authentik.events.system_tasks import SystemTask, prefill_task
from authentik.policies.reputation.apps import CACHE_KEY_PREFIX
from authentik.policies.reputation.models import Reputation
from authentik.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task(bind=True, base=SystemTask)
@prefill_task
def save_reputation(self: SystemTask):
"""Save currently cached reputation to database"""
objects_to_update = []
for _, score in cache.get_many(cache.keys(CACHE_KEY_PREFIX + "*")).items():
rep, _ = Reputation.objects.get_or_create(
ip=score["ip"],
identifier=score["identifier"],
)
rep.ip_geo_data = GEOIP_CONTEXT_PROCESSOR.city_dict(score["ip"]) or {}
rep.ip_asn_data = ASN_CONTEXT_PROCESSOR.asn_dict(score["ip"]) or {}
rep.score = score["score"]
objects_to_update.append(rep)
Reputation.objects.bulk_update(objects_to_update, ["score", "ip_geo_data"])
self.set_status(TaskStatus.SUCCESSFUL, "Successfully updated Reputation")

View File

@ -1,11 +1,14 @@
"""test reputation signals and policy"""
from django.core.cache import cache
from django.test import RequestFactory, TestCase
from authentik.core.models import User
from authentik.lib.generators import generate_id
from authentik.policies.reputation.api import ReputationPolicySerializer
from authentik.policies.reputation.apps import CACHE_KEY_PREFIX
from authentik.policies.reputation.models import Reputation, ReputationPolicy
from authentik.policies.reputation.tasks import save_reputation
from authentik.policies.types import PolicyRequest
from authentik.stages.password import BACKEND_INBUILT
from authentik.stages.password.stage import authenticate
@ -19,6 +22,8 @@ class TestReputationPolicy(TestCase):
self.request = self.request_factory.get("/")
self.test_ip = "127.0.0.1"
self.test_username = "test"
keys = cache.keys(CACHE_KEY_PREFIX + "*")
cache.delete_many(keys)
# We need a user for the one-to-one in userreputation
self.user = User.objects.create(username=self.test_username)
self.backends = [BACKEND_INBUILT]
@ -29,6 +34,13 @@ class TestReputationPolicy(TestCase):
authenticate(
self.request, self.backends, username=self.test_username, password=self.test_username
)
# Test value in cache
self.assertEqual(
cache.get(CACHE_KEY_PREFIX + self.test_ip + "/" + self.test_username),
{"ip": "127.0.0.1", "identifier": "test", "score": -1},
)
# Save cache and check db values
save_reputation.delay().get()
self.assertEqual(Reputation.objects.get(ip=self.test_ip).score, -1)
def test_user_reputation(self):
@ -37,16 +49,14 @@ class TestReputationPolicy(TestCase):
authenticate(
self.request, self.backends, username=self.test_username, password=self.test_username
)
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, -1)
def test_update_reputation(self):
"""test reputation update"""
Reputation.objects.create(identifier=self.test_username, ip=self.test_ip, score=43)
# Trigger negative reputation
authenticate(
self.request, self.backends, username=self.test_username, password=self.test_username
# Test value in cache
self.assertEqual(
cache.get(CACHE_KEY_PREFIX + self.test_ip + "/" + self.test_username),
{"ip": "127.0.0.1", "identifier": "test", "score": -1},
)
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, 42)
# Save cache and check db values
save_reputation.delay().get()
self.assertEqual(Reputation.objects.get(identifier=self.test_username).score, -1)
def test_policy(self):
"""Test Policy"""

View File

@ -5,11 +5,11 @@ from django.db.models.query import Q
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.providers.ldap.models import LDAPProvider

View File

@ -7,11 +7,12 @@ from guardian.utils import get_anonymous_user
from rest_framework import mixins
from rest_framework.fields import CharField, ListField, SerializerMethodField
from rest_framework.filters import OrderingFilter, SearchFilter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import MetaNameSerializer, ModelSerializer
from authentik.core.api.utils import MetaNameSerializer
from authentik.providers.oauth2.api.providers import OAuth2ProviderSerializer
from authentik.providers.oauth2.models import AccessToken, AuthorizationCode, RefreshToken

View File

@ -4,10 +4,9 @@ from urllib.parse import urlencode
from django.urls import reverse
from authentik.core.models import Application, Group
from authentik.core.models import Application
from authentik.core.tests.utils import create_test_admin_user, create_test_brand, create_test_flow
from authentik.lib.generators import generate_id
from authentik.policies.models import PolicyBinding
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.tests.utils import OAuthTestCase
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
@ -78,23 +77,3 @@ class TesOAuth2DeviceInit(OAuthTestCase):
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code}),
)
def test_device_init_denied(self):
"""Test device init"""
group = Group.objects.create(name="foo")
PolicyBinding.objects.create(
group=group,
target=self.application,
order=0,
)
token = DeviceToken.objects.create(
user_code="foo",
provider=self.provider,
)
res = self.client.get(
reverse("authentik_providers_oauth2_root:device-login")
+ "?"
+ urlencode({QS_KEY_CODE: token.user_code})
)
self.assertEqual(res.status_code, 200)
self.assertIn(b"Permission denied", res.content)

View File

@ -11,11 +11,10 @@ from django.views.decorators.csrf import csrf_exempt
from rest_framework.throttling import AnonRateThrottle
from structlog.stdlib import get_logger
from authentik.core.models import Application
from authentik.lib.config import CONFIG
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE
from authentik.providers.oauth2.views.device_init import QS_KEY_CODE, get_application
LOGGER = get_logger()
@ -38,9 +37,7 @@ class DeviceView(View):
).first()
if not provider:
return HttpResponseBadRequest()
try:
_ = provider.application
except Application.DoesNotExist:
if not get_application(provider):
return HttpResponseBadRequest()
self.provider = provider
self.client_id = client_id

View File

@ -1,9 +1,8 @@
"""Device flow views"""
from typing import Any
from django.http import HttpRequest, HttpResponse
from django.utils.translation import gettext as _
from django.views import View
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, IntegerField
from structlog.stdlib import get_logger
@ -17,8 +16,7 @@ from authentik.flows.planner import PLAN_CONTEXT_APPLICATION, PLAN_CONTEXT_SSO,
from authentik.flows.stage import ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_PLAN
from authentik.lib.utils.urls import redirect_with_qs
from authentik.policies.views import PolicyAccessView
from authentik.providers.oauth2.models import DeviceToken
from authentik.providers.oauth2.models import DeviceToken, OAuth2Provider
from authentik.providers.oauth2.views.device_finish import (
PLAN_CONTEXT_DEVICE,
OAuthDeviceCodeFinishStage,
@ -33,52 +31,60 @@ LOGGER = get_logger()
QS_KEY_CODE = "code" # nosec
class CodeValidatorView(PolicyAccessView):
"""Helper to validate frontside token"""
def __init__(self, code: str, **kwargs: Any) -> None:
super().__init__(**kwargs)
self.code = code
def resolve_provider_application(self):
self.token = DeviceToken.objects.filter(user_code=self.code).first()
if not self.token:
raise Application.DoesNotExist
self.provider = self.token.provider
self.application = self.token.provider.application
def get(self, request: HttpRequest, *args, **kwargs):
scope_descriptions = UserInfoView().get_scope_descriptions(self.token.scope, self.provider)
planner = FlowPlanner(self.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: self.application,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: self.token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": self.application.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
def get_application(provider: OAuth2Provider) -> Application | None:
"""Get application from provider"""
try:
app = provider.application
if not app:
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=self.token.provider.authorization_flow.slug,
return app
except Application.DoesNotExist:
return None
def validate_code(code: int, request: HttpRequest) -> HttpResponse | None:
"""Validate user token"""
token = DeviceToken.objects.filter(
user_code=code,
).first()
if not token:
return None
app = get_application(token.provider)
if not app:
return None
scope_descriptions = UserInfoView().get_scope_descriptions(token.scope, token.provider)
planner = FlowPlanner(token.provider.authorization_flow)
planner.allow_empty_flows = True
planner.use_cache = False
try:
plan = planner.plan(
request,
{
PLAN_CONTEXT_SSO: True,
PLAN_CONTEXT_APPLICATION: app,
# OAuth2 related params
PLAN_CONTEXT_DEVICE: token,
# Consent related params
PLAN_CONTEXT_CONSENT_HEADER: _("You're about to sign into %(application)s.")
% {"application": app.name},
PLAN_CONTEXT_CONSENT_PERMISSIONS: scope_descriptions,
},
)
except FlowNonApplicableException:
LOGGER.warning("Flow not applicable to user")
return None
plan.insert_stage(in_memory_stage(OAuthDeviceCodeFinishStage))
request.session[SESSION_KEY_PLAN] = plan
return redirect_with_qs(
"authentik_core:if-flow",
request.GET,
flow_slug=token.provider.authorization_flow.slug,
)
class DeviceEntryView(PolicyAccessView):
class DeviceEntryView(View):
"""View used to initiate the device-code flow, url entered by endusers"""
def dispatch(self, request: HttpRequest) -> HttpResponse:
@ -88,9 +94,7 @@ class DeviceEntryView(PolicyAccessView):
LOGGER.info("Brand has no device code flow configured", brand=brand)
return HttpResponse(status=404)
if QS_KEY_CODE in request.GET:
validation = CodeValidatorView(request.GET[QS_KEY_CODE], request=request).dispatch(
request
)
validation = validate_code(request.GET[QS_KEY_CODE], request)
if validation:
return validation
LOGGER.info("Got code from query parameter but no matching token found")
@ -127,7 +131,7 @@ class OAuthDeviceCodeChallengeResponse(ChallengeResponse):
def validate_code(self, code: int) -> HttpResponse | None:
"""Validate code and save the returned http response"""
response = CodeValidatorView(code, request=self.stage.request).dispatch(self.stage.request)
response = validate_code(code, self.stage.request)
if not response:
raise ValidationError(_("Invalid code"), "invalid")
return response

View File

@ -6,11 +6,12 @@ from django.utils.translation import gettext_lazy as _
from drf_spectacular.utils import extend_schema_field
from rest_framework.exceptions import ValidationError
from rest_framework.fields import CharField, ListField, ReadOnlyField, SerializerMethodField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer, PassiveSerializer
from authentik.core.api.utils import PassiveSerializer
from authentik.lib.utils.time import timedelta_from_string
from authentik.providers.oauth2.models import ScopeMapping
from authentik.providers.oauth2.views.provider import ProviderInfoView

View File

@ -1,11 +1,11 @@
"""RadiusProvider API Views"""
from rest_framework.fields import CharField, ListField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import ModelSerializer
from authentik.providers.radius.models import RadiusProvider

View File

@ -268,7 +268,7 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
except ValueError as exc: # pragma: no cover
LOGGER.warning(str(exc))
raise ValidationError(
_("Failed to import Metadata: {messages}".format_map({"messages": str(exc)})),
_("Failed to import Metadata: {messages}".format_map({"message": str(exc)})),
) from None
return Response(status=204)

View File

@ -1,12 +1,11 @@
"""SCIMProviderGroup API Views"""
from rest_framework import mixins
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserGroupSerializer
from authentik.core.api.utils import ModelSerializer
from authentik.lib.sync.outgoing.api import OutgoingSyncConnectionCreateMixin
from authentik.providers.scim.models import SCIMProviderGroup
@ -29,7 +28,6 @@ class SCIMProviderGroupSerializer(ModelSerializer):
class SCIMProviderGroupViewSet(
mixins.CreateModelMixin,
OutgoingSyncConnectionCreateMixin,
mixins.RetrieveModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,

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