Compare commits

...

206 Commits

Author SHA1 Message Date
ab2b13938e release: 2021.10.3 2021-11-08 20:52:11 +01:00
5c97a3aef3 website/docs: final 2021.10.3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 20:51:59 +01:00
e6963c543d outpost: remove analytics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 19:35:53 +01:00
9ca15983a2 root: keep last 30 backups
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 17:46:25 +01:00
99ef94b7aa stages/prompt: only set placeholder when in context
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 15:10:32 +01:00
133bedafba web: Update Web API Client version (#1752)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-08 14:53:50 +01:00
c3faa61ed9 stages/prompt: set field placeholder based on plan context
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 14:47:50 +01:00
da74304221 stages/prompt: add text_read_only field
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 14:45:56 +01:00
ed6659a46d outpost/ldap: don't cleanup user info as it is overwritten on bind
closes #1651

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-08 14:41:38 +01:00
0abb1f94a4 build(deps): bump @sentry/tracing from 6.14.0 to 6.14.1 in /web (#1746) 2021-11-08 08:30:37 +01:00
c7e299e0bf build(deps): bump eslint from 8.1.0 to 8.2.0 in /web (#1747) 2021-11-08 08:29:44 +01:00
8a6590bac8 build(deps): bump @sentry/browser from 6.14.0 to 6.14.1 in /web (#1748) 2021-11-08 08:29:35 +01:00
ed717dcfa2 build(deps): bump boto3 from 1.19.11 to 1.19.12 (#1749) 2021-11-08 08:29:23 +01:00
b6df42f580 build(deps): bump goauthentik.io/api from 0.2021102.5 to 0.2021102.6 (#1750) 2021-11-08 08:29:16 +01:00
2ea85bd0c4 build(deps): bump celery from 5.1.2 to 5.2.0 (#1751) 2021-11-08 08:28:33 +01:00
68fa8105e1 Merge branch 'master' into version-2021.10 2021-11-07 23:16:53 +01:00
79db0ce4c1 stages/prompt: use initial instead of default
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 22:20:08 +01:00
5e23b11764 stages/prompt: default prompts to the current value of the context
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 21:59:50 +01:00
c4e029ffe2 recovery: add create_admin_group management command
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 21:53:21 +01:00
61b5b36192 core: add command to output full config
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 21:45:52 +01:00
c6cc1b1728 root: fix defaults for EMAIL_USE_TLS
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 21:37:14 +01:00
77dd652160 web: Update Web API Client version (#1744)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
# Conflicts:
#	web/package-lock.json
#	web/package.json
2021-11-07 18:04:27 +01:00
1144944adb stages/identification: only allow limited challenges for login sources
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:35 +01:00
7751be284e stages/identification: use random sleep
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:32 +01:00
74382c6287 cmd/server: improve cleanup on shutdown
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:29 +01:00
011babbbd9 web: fix linting errors by adding a wrapper for next param
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:26 +01:00
3c01a1dd7b ci: keep latest github and scripts folder
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:22 +01:00
6e832be2de core: fix auth_method for tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:19 +01:00
46017f2f86 events: ignore creation/deletion of AuthenticatedSession objects
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:16 +01:00
da50eb0369 web/user: fix redirect after starting configuration flow from user interface
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:13 +01:00
b996e3cee7 ci: lock pipenv to 2021.11.5 for now
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:09 +01:00
12735cc14c admin: improve check to remove version notifications
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-07 18:03:06 +01:00
4d36699b78 outpost/ldap: cleanup
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 19:32:11 +01:00
8110d2861b web: Update Web API Client version (#1744)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-06 19:23:25 +01:00
1cc60f572d root: use forked openapi-generator
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 19:10:17 +01:00
90151a13ae stages/identification: only allow limited challenges for login sources
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 14:53:38 +01:00
f958aa6930 stages/identification: use random sleep
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 14:50:35 +01:00
13fbac30a2 cmd/server: improve cleanup on shutdown
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 13:54:59 +01:00
4f4cdf16f1 web: fix linting errors by adding a wrapper for next param
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 13:19:39 +01:00
7d75599627 ci: keep latest github and scripts folder
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 13:06:51 +01:00
924a13e832 core: fix auth_method for tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 13:04:41 +01:00
ae83c35dfd events: ignore creation/deletion of AuthenticatedSession objects
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 13:04:34 +01:00
e9102f4e28 web/user: fix redirect after starting configuration flow from user interface
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 12:57:06 +01:00
9b8c1cbea5 ci: lock pipenv to 2021.11.5 for now
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 12:22:59 +01:00
6424bf98da admin: improve check to remove version notifications
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-06 12:15:05 +01:00
74fb0f9e2a website/docs: Document using Proxmox VE as Provider Integration (#1743) 2021-11-06 00:00:57 +01:00
4380f37a77 web/admin: fix missing values for LDAP Provider form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 11:42:09 +01:00
17fccd44e6 build(deps): bump goauthentik.io/api from 0.2021102.4 to 0.2021102.5 (#1742)
Bumps [goauthentik.io/api](https://github.com/goauthentik/client-go) from 0.2021102.4 to 0.2021102.5.
- [Release notes](https://github.com/goauthentik/client-go/releases)
- [Commits](https://github.com/goauthentik/client-go/compare/v0.2021102.4...v0.2021102.5)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-11-05 10:50:59 +01:00
217a8b5610 web: Update Web API Client version (#1741)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-05 10:42:02 +01:00
2cef220a3e providers/ldap: add/squash migrations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 10:41:50 +01:00
5a8c66d325 providers/ldap: memory Query (#1681)
* outposts/ldap: modularise ldap outpost, to allow different searchers and binders

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

* outposts/ldap: add basic in-memory searcher

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

* providers/ldap: add search mode field

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

* outpost: add search mode field

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 10:37:30 +01:00
8de13d3f67 build(deps): bump goauthentik.io/api from 0.2021102.2 to 0.2021102.4 (#1738)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 09:33:26 +01:00
5c22bedbaf build(deps): bump golang from 1.17.2-bullseye to 1.17.3-bullseye (#1737) 2021-11-05 08:29:51 +01:00
8a0f993f0b build(deps): bump boto3 from 1.19.10 to 1.19.11 (#1739) 2021-11-05 08:29:37 +01:00
abcf515a69 web/admin: also show note for SAML provider
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 01:21:10 +01:00
894f704c27 web: Update Web API Client version (#1736)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-05 01:18:13 +01:00
7798292aa8 sources/plex: use exception_to_string in tasks
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 01:16:36 +01:00
3005ca17bd web/admin: show warning on provider when not used with outpost
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 01:15:33 +01:00
909461e533 providers/*: include list of outposts
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 01:06:04 +01:00
df838a4023 web: Update Web API Client version (#1735)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-05 01:01:51 +01:00
0f86b62dd3 website/docs: prepare 2021.10.3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 00:55:31 +01:00
a40c3aeb68 core: make group membership lookup respect parent groups (upwards)
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 00:53:56 +01:00
4080738ded web/elements: fix import
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 00:27:23 +01:00
4a89be3048 core: include parent group name
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-05 00:11:17 +01:00
e587c53e18 web: remove deprecated rollup-plugin-node-resolve
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 22:34:48 +01:00
023b97aa69 sources/ldap: remove deprecated default
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 22:13:47 +01:00
51365dba74 web: Update Web API Client version (#1734)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-04 21:03:09 +01:00
0d3705685e web/admin: show warning on invitation list when no stage exists or is bound
closes #1720

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 21:00:42 +01:00
738e4d5c74 web/admin: only show flows with an invitation stage configured instead of all enrollment flows
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1720
2021-11-04 20:54:55 +01:00
b14b9cb0dd tests/e2e: fix selector for static token tests
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:55:30 +01:00
2a21ebf7b0 web/flows: fix authenticator_validate not allowing alpha-numeric codes due to empty pattern
closes #1663

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:39:10 +01:00
5bc1301043 stages/authenticator_*: add default name for authenticators
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:34:51 +01:00
e0e4bf6972 web/user: fix device type for static tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:28:55 +01:00
337677ad12 web/flows: improve display of static tokens
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:21:04 +01:00
3712d5aee2 web/user: fix empty page when no sources to connect exist
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:16:08 +01:00
dd82d55725 outposts: also send outpost type
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 15:08:31 +01:00
8d766efecb root: don't set signal on start when running in ci or dev
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 14:32:21 +01:00
9ac3b29418 outpost: add lightweight, anonymous metrics
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 14:10:43 +01:00
5000c5b061 web/user: fix ak-user-settings-password getting wrong configureUrl
closes #1733

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 13:50:26 +01:00
b362d2af03 lib: fix linting issue
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 13:43:09 +01:00
bcd42fce13 root: further improve detection of environment we're running in
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 13:35:22 +01:00
6deddd038f internal: start embedded outpost directly after backend is healthy instead of waiting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 13:18:04 +01:00
3b47cb64da root: improve compose detection, add anonymous stats
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 13:16:21 +01:00
cf5e70c759 lifecycle: revert to non-h11 worker
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-04 13:16:10 +01:00
20bc38a54b build(deps): bump boto3 from 1.19.9 to 1.19.10 (#1731) 2021-11-04 08:36:15 +01:00
672a4ab1f4 build(deps): bump @sentry/browser from 6.13.3 to 6.14.0 in /web (#1728) 2021-11-04 08:35:53 +01:00
47dd667261 build(deps): bump @trivago/prettier-plugin-sort-imports in /web (#1729) 2021-11-04 08:35:40 +01:00
d1ac69789b build(deps): bump @sentry/tracing from 6.13.3 to 6.14.0 in /web (#1730) 2021-11-04 08:34:36 +01:00
08abf81c6d build(deps): bump goauthentik.io/api from 0.2021101.11 to 0.2021102.2 (#1732) 2021-11-04 08:33:50 +01:00
76bd987e6f web: Update Web API Client version (#1727)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-03 21:11:10 +01:00
5374352411 sources/plex: allow users to connect their plex account without login flow
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 21:09:02 +01:00
08eff4cc5d sources/plex: fix missing ordering
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 20:21:33 +01:00
c87a9f9489 web: remove debug entry
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 20:19:37 +01:00
8f6d700aa8 sources/oauth: set prompt=none for Discord provider
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 15:48:59 +01:00
c6843b026c web: Update Web API Client version (#1726)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-03 11:41:57 +01:00
3769c33ef0 Merge branch 'version-2021.10' 2021-11-03 11:39:27 +01:00
8982afaf44 website/docs: add 2021.10.2
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 10:20:29 +01:00
58c221e867 release: 2021.10.2 2021-11-03 10:07:28 +01:00
108d3e56e3 build(deps): bump @docusaurus/plugin-client-redirects in /website (#1721) 2021-11-03 09:01:54 +01:00
145b32c480 build(deps): bump lxml from 4.6.3 to 4.6.4 (#1725) 2021-11-03 08:33:42 +01:00
c788504bb0 build(deps): bump boto3 from 1.19.8 to 1.19.9 (#1724) 2021-11-03 08:33:32 +01:00
34782b31e5 build(deps): bump @docusaurus/preset-classic in /website (#1722) 2021-11-03 08:33:10 +01:00
5a3ca13d76 build(deps): bump swagger-spec-validator from 2.7.3 to 2.7.4 (#1723) 2021-11-03 08:33:02 +01:00
5dc0f3b91b website: remove static service account, use helm chart instead
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 00:14:01 +01:00
f51515f3de stages/invitation: don't throw 404 error in stage
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-03 00:07:13 +01:00
f978575293 stages/invitation: remove invitation from plan context after deletion
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 23:50:12 +01:00
cb64eed90d web: re-fix lint
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 14:59:35 +01:00
db1f7f0400 Revert "tests/e2e: remove deprecated desired_capabilities"
This reverts commit 2da7a8fede.
2021-11-02 14:53:52 +01:00
0d02dbf55c api: replace django sentry proxy with go proxy to prevent login issues
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 14:44:37 +01:00
6da78b8c32 web/user: fix linting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 13:48:37 +01:00
3a80bc8bda web/user: fix configureUrl not being passed to <ak-user-settings-password>
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 13:21:01 +01:00
1aa9c0f9ca root: move pyright config to toml
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 13:05:11 +01:00
2da7a8fede tests/e2e: remove deprecated desired_capabilities
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 11:04:47 +01:00
89cb402f42 root: fix postgres install on bullseye
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 10:56:39 +01:00
b617fd213f web: replace deprecated commonjs plugin
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 10:38:20 +01:00
97b0f58f25 root: build outposts without CGO, use debug images for basic shell
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-02 10:11:51 +01:00
49a98bb744 build(deps): bump @typescript-eslint/eslint-plugin in /web (#1715) 2021-11-02 08:45:25 +01:00
f93a00d773 build(deps): bump rollup from 2.58.3 to 2.59.0 in /web (#1716) 2021-11-02 08:44:16 +01:00
8de40a8a21 build(deps): bump @typescript-eslint/parser from 5.2.0 to 5.3.0 in /web (#1717) 2021-11-02 08:44:08 +01:00
b9c54e97fa build(deps): bump goauthentik.io/api from 0.2021101.10 to 0.2021101.11 (#1718) 2021-11-02 08:44:00 +01:00
f1c55465f7 build(deps): bump boto3 from 1.19.7 to 1.19.8 (#1719) 2021-11-02 08:43:51 +01:00
40c2b2860b web: Update Web API Client version (#1712)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-11-01 21:08:38 +01:00
a92bce322d web/flows: fix sub_text not rendering for static fields
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-01 20:54:43 +01:00
af83308fd4 stages/prompt: fix type in Prompt not having enum set
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-01 20:44:48 +01:00
73d991e75a root: update to buster
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-01 16:31:26 +01:00
1eba3f1334 root: update base images for outposts
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-01 16:29:53 +01:00
b86251255d website/docs: add azure ad docs
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-11-01 15:58:59 +01:00
ccab41a6ca build(deps): bump packaging from 21.0 to 21.2 (#1708) 2021-11-01 12:32:42 +01:00
0e051031b1 build(deps): bump django-storages from 1.12.2 to 1.12.3 (#1710) 2021-11-01 12:24:06 +01:00
aecbe8c585 build(deps): bump django from 3.2.8 to 3.2.9 (#1711) 2021-11-01 12:19:17 +01:00
da98022704 build(deps): bump boto3 from 1.19.6 to 1.19.7 (#1709) 2021-11-01 12:16:20 +01:00
e13f9c0b38 build(deps-dev): bump coverage from 6.0.2 to 6.1.1 (#1707) 2021-11-01 12:15:57 +01:00
7941fb9d95 build(deps): bump @babel/plugin-proposal-decorators in /web (#1703) 2021-11-01 10:05:47 +01:00
d2392b0881 build(deps): bump @babel/preset-env from 7.15.8 to 7.16.0 in /web (#1704) 2021-11-01 10:05:37 +01:00
b2044d75fb build(deps): bump @babel/core from 7.15.8 to 7.16.0 in /web (#1701) 2021-11-01 10:04:54 +01:00
617b64b7db build(deps): bump @babel/preset-typescript from 7.15.0 to 7.16.0 in /web (#1700) 2021-11-01 10:03:54 +01:00
2bf5f2709a build(deps): bump @patternfly/patternfly from 4.144.5 to 4.151.4 in /web (#1702) 2021-11-01 10:03:42 +01:00
f03325df28 build(deps): bump @babel/plugin-transform-runtime in /web (#1705) 2021-11-01 10:03:22 +01:00
2b71e5bdfd build(deps): bump goauthentik.io/api from 0.2021101.5 to 0.2021101.10 (#1706) 2021-11-01 10:02:52 +01:00
f861737b85 web/admin: fix formatting
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 23:04:26 +01:00
6036d88392 providers/proxy: allow configuring of additional scope mappings for proxy
closes #1255

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 22:25:51 +01:00
bfc8a56a0b *: fix tests for new field show_source_labels
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 18:18:21 +01:00
8d995011b8 web: Update Web API Client version (#1699)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-10-31 18:00:20 +01:00
5646141fe2 stages/identification: add show_source_labels option, to show labels for sources
closes #1679

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 17:57:48 +01:00
96b0bc324e web/flows: fix invalid validation for static tokens
closes #1663

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 17:32:39 +01:00
335d6edd11 providers/saml: fix error on missing AssertionConsumerServiceURL, fall back to default ACS
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 17:21:15 +01:00
5d9bed130a root: fix Detection of S3 settings for backups
closes #1698

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 16:54:10 +01:00
0a1ab74707 web: Update Web API Client version (#1697)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-10-31 12:16:09 +01:00
ef24b94585 Merge branch 'version-2021.10' 2021-10-31 12:13:43 +01:00
77b0438aa4 website/docs: prepare 2021.10.1
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 10:57:48 +01:00
2788329880 release: 2021.10.1 2021-10-31 10:56:21 +01:00
15ab11be70 web/user: fix wrong device being selected in user's mfa update form
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-31 00:55:56 +02:00
8d5460a132 outposts: separate websocket re-connection logic to decrease requests on reconnect
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 21:33:50 +02:00
5ba2c80813 Merge branch 'update-web-api-client'
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

# Conflicts:
#	web/src/user/user-settings/mfa/MFADeviceForm.ts
2021-10-30 15:20:16 +02:00
06766bdb25 web/user: update form to update mfa devices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 15:19:17 +02:00
fdae13316c web: Update Web API Client version
Signed-off-by: GitHub <noreply@github.com>
2021-10-30 13:18:31 +00:00
ae21886e8e web/user: update form to update mfa devices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 15:16:27 +02:00
f5dc81907a core: add created field to source connection
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 15:02:41 +02:00
40f8ce3c4c web/user: rework MFA Device UI to support multiple devices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 14:56:14 +02:00
c934915776 web: Update Web API Client version (#1695)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-10-30 14:53:02 +02:00
d70c8fbcc3 core: add API for all user-source connections
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 14:36:54 +02:00
12b26e49ec flows: optimise stage user_settings API
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-30 14:16:28 +02:00
0ac548d56e web: Update Web API Client version (#1693)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-10-29 09:16:08 +02:00
e771e1857f core: add API to list all authenticator devices
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-29 09:08:48 +02:00
479e9750c7 web/user: also search desc and publisher
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-29 09:08:48 +02:00
c5e7801247 build(deps): bump github.com/go-openapi/runtime from 0.20.0 to 0.21.0 (#1691)
Bumps [github.com/go-openapi/runtime](https://github.com/go-openapi/runtime) from 0.20.0 to 0.21.0.
- [Release notes](https://github.com/go-openapi/runtime/releases)
- [Commits](https://github.com/go-openapi/runtime/compare/v0.20.0...v0.21.0)

---
updated-dependencies:
- dependency-name: github.com/go-openapi/runtime
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-29 09:06:34 +02:00
48ea15a946 build(deps): bump flowchart.js from 1.16.0 to 1.17.0 in /web (#1689) 2021-10-29 08:32:13 +02:00
e4c06f7356 build(deps): bump boto3 from 1.19.5 to 1.19.6 (#1690) 2021-10-29 08:31:58 +02:00
4d7d866e4b build(deps): bump github.com/go-openapi/strfmt from 0.20.3 to 0.21.0 (#1692) 2021-10-29 08:31:34 +02:00
72a93c0959 root: pin node images to v16
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-28 10:29:48 +02:00
73733b20b6 build(deps): bump @trivago/prettier-plugin-sort-imports from 2.0.4 to 3.0.0 in /web (#1684)
* build(deps): bump @trivago/prettier-plugin-sort-imports in /web

Bumps [@trivago/prettier-plugin-sort-imports](https://github.com/trivago/prettier-plugin-sort-imports) from 2.0.4 to 3.0.0.
- [Release notes](https://github.com/trivago/prettier-plugin-sort-imports/releases)
- [Changelog](https://github.com/trivago/prettier-plugin-sort-imports/blob/master/CHANGELOG.md)
- [Commits](https://github.com/trivago/prettier-plugin-sort-imports/commits)

---
updated-dependencies:
- dependency-name: "@trivago/prettier-plugin-sort-imports"
  dependency-type: direct:production
  update-type: version-update:semver-major
...

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

* web: update prettier config

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-28 09:48:51 +02:00
3872314931 root: update golang ldap server package
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-28 09:48:31 +02:00
85c6ede448 build(deps): bump goauthentik.io/api from 0.2021101.4 to 0.2021101.5 (#1685) 2021-10-28 08:47:04 +02:00
49c2bee9d6 build(deps): bump boto3 from 1.19.4 to 1.19.5 (#1686) 2021-10-28 08:46:54 +02:00
6b2c9d7c44 web: Update Web API Client version (#1683)
Signed-off-by: GitHub <noreply@github.com>

Co-authored-by: BeryJu <BeryJu@users.noreply.github.com>
2021-10-27 22:32:01 +02:00
381010600f release: 2021.10.1-rc3 2021-10-27 18:57:07 +02:00
2a265f706a website/docs: prepare 2021.10.1-rc3
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 18:56:52 +02:00
1b21b50b77 providers/oauth2: fallback to uid if UPN was selected but isn't available
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 16:11:35 +02:00
fa6324ab1d sources/ldap: prevent key users from being set as this is an M2M relation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 15:43:56 +02:00
9e0daf2bcf sources/ldap: skip values which are of type bytes
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 15:43:38 +02:00
0273ae16df events: fix error when notification transport doesn't exist anymore
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 15:38:41 +02:00
f2f12ef0ba api: fix error when connection to websocket via secret_key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 15:21:52 +02:00
61d3df5f02 outposts: fix docker controller not using object_naming_template
closes #1682

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-27 15:04:59 +02:00
971de4fcb9 core: add USER_ATTRIBUTE_CHANGE_EMAIL
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>

#1590 and #1677
2021-10-27 13:34:11 +02:00
9c0bc78ca0 build(deps): bump boto3 from 1.19.3 to 1.19.4 (#1678) 2021-10-27 08:30:26 +02:00
92085f1a3c core: add toggle to completely disable backup mechanism
closes #1671

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-26 17:04:33 +02:00
6067406e96 website/docs: add freeipa docs (#1666)
* website/docs: add freeipa docs

Signed-off-by: M. David Bennett <mdavidbennett@syntheticworks.com>

* website/docs: fix freeipa settings screenshot

Signed-off-by: M. David Bennett <mdavidbennett@syntheticworks.com>
2021-10-26 14:29:42 +02:00
9ccd4d69fe build(deps): bump @typescript-eslint/eslint-plugin in /web (#1673) 2021-10-26 13:25:45 +02:00
17ec48332d build(deps): bump rollup from 2.58.0 to 2.58.3 in /web (#1672) 2021-10-26 13:25:01 +02:00
d3f5253a6b build(deps): bump @typescript-eslint/parser from 5.1.0 to 5.2.0 in /web (#1674) 2021-10-26 13:24:38 +02:00
7a70726d57 build(deps): bump kubernetes from v19.15.0b1 to 19.15.0 (#1675) 2021-10-26 13:24:12 +02:00
be303937fb build(deps): bump boto3 from 1.19.2 to 1.19.3 (#1676) 2021-10-26 13:23:59 +02:00
2326fc9ae2 build(deps): bump eslint from 8.0.1 to 8.1.0 in /web (#1669) 2021-10-25 09:58:30 +02:00
9374b0bcf2 build(deps): bump chart.js from 3.5.1 to 3.6.0 in /web (#1668) 2021-10-25 09:58:18 +02:00
47e6028099 build(deps): bump boto3 from 1.19.1 to 1.19.2 (#1670) 2021-10-25 09:57:57 +02:00
24114e8304 Update harbor doc (#1667) 2021-10-24 16:44:04 +02:00
921d9c79a1 build(deps): bump @docusaurus/plugin-client-redirects in /website (#1659) 2021-10-22 07:35:06 +02:00
1119989ab7 build(deps): bump postcss from 8.3.10 to 8.3.11 in /website (#1657) 2021-10-22 07:34:34 +02:00
e17594f0f7 build(deps): bump eslint-plugin-custom-elements in /web (#1658) 2021-10-22 07:34:25 +02:00
5ae3b868d4 build(deps): bump @docusaurus/preset-classic in /website (#1660) 2021-10-22 07:34:11 +02:00
37ee4af5ff build(deps): bump goauthentik.io/api from 0.2021101.2 to 0.2021101.4 (#1661) 2021-10-22 07:33:55 +02:00
829aaca317 build(deps): bump boto3 from 1.19.0 to 1.19.1 (#1662) 2021-10-22 07:33:45 +02:00
8eb4d53810 providers/oauth2: fix events being created from /application/o/authorize/
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-21 22:59:01 +02:00
e60dfc5b3c web: Update Web API Client version (#1656) 2021-10-21 17:06:53 +02:00
cc403d8777 Merge branch 'version-2021.10' 2021-10-21 16:52:18 +02:00
b81e2e69d1 website/docs: prepare 2021.10.1-rc2
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
2021-10-21 16:49:11 +02:00
404 changed files with 8178 additions and 10804 deletions

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 2021.10.1-rc2
current_version = 2021.10.3
tag = True
commit = True
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)

View File

@ -147,6 +147,8 @@ jobs:
# Copy current, latest config to local
cp authentik/lib/default.yml local.env.yml
git checkout $(git describe --abbrev=0 --match 'version/*')
git checkout ${{ steps.ev.outputs.branchName }} -- .github
git checkout ${{ steps.ev.outputs.branchName }} -- scripts
- name: prepare
env:
INSTALL: ${{ steps.cache-pipenv.outputs.cache-hit }}

View File

@ -30,14 +30,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik:2021.10.1-rc2,
beryju/authentik:2021.10.3,
beryju/authentik:latest,
ghcr.io/goauthentik/server:2021.10.1-rc2,
ghcr.io/goauthentik/server:2021.10.3,
ghcr.io/goauthentik/server:latest
platforms: linux/amd64,linux/arm64
context: .
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.10.3', 'rc') }}
run: |
docker pull beryju/authentik:latest
docker tag beryju/authentik:latest beryju/authentik:stable
@ -72,14 +72,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-proxy:2021.10.1-rc2,
beryju/authentik-proxy:2021.10.3,
beryju/authentik-proxy:latest,
ghcr.io/goauthentik/proxy:2021.10.1-rc2,
ghcr.io/goauthentik/proxy:2021.10.3,
ghcr.io/goauthentik/proxy:latest
file: proxy.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.10.3', 'rc') }}
run: |
docker pull beryju/authentik-proxy:latest
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
@ -114,14 +114,14 @@ jobs:
with:
push: ${{ github.event_name == 'release' }}
tags: |
beryju/authentik-ldap:2021.10.1-rc2,
beryju/authentik-ldap:2021.10.3,
beryju/authentik-ldap:latest,
ghcr.io/goauthentik/ldap:2021.10.1-rc2,
ghcr.io/goauthentik/ldap:2021.10.3,
ghcr.io/goauthentik/ldap:latest
file: ldap.Dockerfile
platforms: linux/amd64,linux/arm64
- name: Building Docker Image (stable)
if: ${{ github.event_name == 'release' && !contains('2021.10.1-rc2', 'rc') }}
if: ${{ github.event_name == 'release' && !contains('2021.10.3', 'rc') }}
run: |
docker pull beryju/authentik-ldap:latest
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
@ -170,7 +170,7 @@ jobs:
SENTRY_PROJECT: authentik
SENTRY_URL: https://sentry.beryju.org
with:
version: authentik@2021.10.1-rc2
version: authentik@2021.10.3
environment: beryjuorg-prod
sourcemaps: './web/dist'
url_prefix: '~/static/dist'

View File

@ -1,17 +1,17 @@
# Stage 1: Lock python dependencies
FROM docker.io/python:3.9-slim-buster as locker
FROM docker.io/python:3.9-bullseye as locker
COPY ./Pipfile /app/
COPY ./Pipfile.lock /app/
WORKDIR /app/
RUN pip install pipenv && \
RUN pip install pipenv==2021.5.29 && \
pipenv lock -r > requirements.txt && \
pipenv lock -r --dev-only > requirements-dev.txt
# Stage 2: Build website
FROM docker.io/node as website-builder
FROM docker.io/node:16 as website-builder
COPY ./website /static/
@ -19,7 +19,7 @@ ENV NODE_ENV=production
RUN cd /static && npm i && npm run build-docs-only
# Stage 3: Build webui
FROM docker.io/node as web-builder
FROM docker.io/node:16 as web-builder
COPY ./web /static/
@ -27,7 +27,7 @@ ENV NODE_ENV=production
RUN cd /static && npm i && npm run build
# Stage 4: Build go proxy
FROM docker.io/golang:1.17.2 AS builder
FROM docker.io/golang:1.17.3-bullseye AS builder
WORKDIR /work
@ -47,7 +47,7 @@ COPY ./go.sum /work/go.sum
RUN go build -o /work/authentik ./cmd/server/main.go
# Stage 5: Run
FROM docker.io/python:3.9-slim-buster
FROM docker.io/python:3.9-bullseye
WORKDIR /
COPY --from=locker /app/requirements.txt /
@ -59,7 +59,7 @@ ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
RUN apt-get update && \
apt-get install -y --no-install-recommends curl ca-certificates gnupg git runit && \
curl https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
echo "deb http://apt.postgresql.org/pub/repos/apt buster-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
echo "deb http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" > /etc/apt/sources.list.d/pgdg.list && \
apt-get update && \
apt-get install -y --no-install-recommends libpq-dev postgresql-client build-essential libxmlsec1-dev pkg-config libmaxminddb0 && \
pip install -r /requirements.txt --no-cache-dir && \

View File

@ -30,7 +30,6 @@ lint-fix:
website/developer-docs
lint:
pyright authentik tests lifecycle
bandit -r authentik tests lifecycle -x node_modules
pylint authentik tests lifecycle
@ -49,7 +48,7 @@ gen-web:
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
openapitools/openapi-generator-cli generate \
ghcr.io/beryju/openapi-generator generate \
-i /local/schema.yml \
-g typescript-fetch \
-o /local/web-api \
@ -61,18 +60,19 @@ gen-web:
\cp -rfv web-api/* web/node_modules/@goauthentik/api
gen-outpost:
wget https://raw.githubusercontent.com/goauthentik/client-go/main/config.yaml -O config.yaml
mkdir -p templates
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/README.mustache -O templates/README.mustache
wget https://raw.githubusercontent.com/goauthentik/client-go/main/templates/go.mod.mustache -O templates/go.mod.mustache
docker run \
--rm -v ${PWD}:/local \
--user ${UID}:${GID} \
openapitools/openapi-generator-cli generate \
--git-host goauthentik.io \
--git-repo-id outpost \
--git-user-id api \
-i /local/schema.yml \
-g go \
-o /local/api \
--additional-properties=packageName=api,enumClassPrefix=true,useOneOfDiscriminatorLookup=true,disallowAdditionalPropertiesIfNotPresent=false
rm -f api/go.mod api/go.sum
-c /local/config.yaml
go mod edit -replace goauthentik.io/api=./api
gen: gen-build gen-clean gen-web

View File

@ -26,7 +26,7 @@ drf-spectacular = "*"
facebook-sdk = "*"
geoip2 = "*"
gunicorn = "*"
kubernetes = "==v19.15.0b1"
kubernetes = "==v19.15.0"
ldap3 = "*"
lxml = "*"
packaging = "*"

909
Pipfile.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,3 @@
"""authentik"""
__version__ = "2021.10.1-rc2"
__version__ = "2021.10.3"
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"

View File

@ -27,6 +27,7 @@ VERSION_CACHE_TIMEOUT = 8 * 60 * 60 # 8 hours
# Chop of the first ^ because we want to search the entire string
URL_FINDER = URLValidator.regex.pattern[1:]
PROM_INFO = Info("authentik_version", "Currently running authentik version")
LOCAL_VERSION = parse(__version__)
def _set_prom_info():
@ -48,7 +49,7 @@ def clear_update_notifications():
if "new_version" not in notification.event.context:
continue
notification_version = notification.event.context["new_version"]
if notification_version == __version__:
if LOCAL_VERSION >= parse(notification_version):
notification.delete()
@ -74,8 +75,7 @@ def update_latest_version(self: MonitoredTask):
_set_prom_info()
# Check if upstream version is newer than what we're running,
# and if no event exists yet, create one.
local_version = parse(__version__)
if local_version < parse(upstream_version):
if LOCAL_VERSION < parse(upstream_version):
# Event has already been created, don't create duplicate
if Event.objects.filter(
action=EventAction.UPDATE_AVAILABLE,

View File

@ -45,7 +45,8 @@ def bearer_auth(raw_header: bytes) -> Optional[User]:
if not user:
raise AuthenticationFailed("Token invalid/expired")
return user
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
if hasattr(LOCAL, "authentik"):
LOCAL.authentik[KEY_AUTH_VIA] = "api_token"
return tokens.first().user
@ -59,7 +60,8 @@ def token_secret_key(value: str) -> Optional[User]:
outposts = Outpost.objects.filter(managed=MANAGED_OUTPOST)
if not outposts:
return None
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
if hasattr(LOCAL, "authentik"):
LOCAL.authentik[KEY_AUTH_VIA] = "secret_key"
outpost = outposts.first()
return outpost.user

View File

@ -1,19 +0,0 @@
"""API tasks"""
from authentik.lib.utils.http import get_http_session
from authentik.root.celery import CELERY_APP
SENTRY_SESSION = get_http_session()
@CELERY_APP.task()
def sentry_proxy(payload: str):
"""Relay data to sentry"""
SENTRY_SESSION.post(
"https://sentry.beryju.org/api/8/envelope/",
data=payload,
headers={
"Content-Type": "application/octet-stream",
},
timeout=10,
)

View File

@ -4,7 +4,7 @@ from django.urls import include, path
from authentik.api.v3.urls import urlpatterns as v3_urls
urlpatterns = [
# Remove in 2022.1
# TODO: Remove in 2022.1
path("v2beta/", include(v3_urls)),
path("v3/", include(v3_urls)),
]

View File

@ -1,65 +0,0 @@
"""Sentry tunnel"""
from json import loads
from django.conf import settings
from django.http.request import HttpRequest
from django.http.response import HttpResponse
from rest_framework.authentication import SessionAuthentication
from rest_framework.parsers import BaseParser
from rest_framework.permissions import AllowAny
from rest_framework.request import Request
from rest_framework.throttling import AnonRateThrottle
from rest_framework.views import APIView
from structlog.stdlib import get_logger
from authentik.api.tasks import sentry_proxy
from authentik.lib.config import CONFIG
LOGGER = get_logger()
class PlainTextParser(BaseParser):
"""Plain text parser."""
media_type = "text/plain"
def parse(self, stream, media_type=None, parser_context=None) -> str:
"""Simply return a string representing the body of the request."""
return stream.read()
class CsrfExemptSessionAuthentication(SessionAuthentication):
"""CSRF-exempt Session authentication"""
def enforce_csrf(self, request: Request):
return # To not perform the csrf check previously happening
class SentryTunnelView(APIView):
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
serializer_class = None
parser_classes = [PlainTextParser]
throttle_classes = [AnonRateThrottle]
permission_classes = [AllowAny]
authentication_classes = [CsrfExemptSessionAuthentication]
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Sentry tunnel, to prevent ad blockers from blocking sentry"""
# Only allow usage of this endpoint when error reporting is enabled
if not CONFIG.y_bool("error_reporting.enabled", False):
LOGGER.debug("error reporting disabled")
return HttpResponse(status=400)
# Body is 2 json objects separated by \n
full_body = request.body
lines = full_body.splitlines()
if len(lines) < 1:
return HttpResponse(status=400)
header = loads(lines[0])
# Check that the DSN is what we expect
dsn = header.get("dsn", "")
if dsn != settings.SENTRY_DSN:
LOGGER.debug("Invalid dsn", have=dsn, expected=settings.SENTRY_DSN)
return HttpResponse(status=400)
sentry_proxy.delay(full_body.decode())
return HttpResponse(status=204)

View File

@ -11,14 +11,14 @@ from authentik.admin.api.tasks import TaskViewSet
from authentik.admin.api.version import VersionView
from authentik.admin.api.workers import WorkerView
from authentik.api.v3.config import ConfigView
from authentik.api.v3.sentry import SentryTunnelView
from authentik.api.views import APIBrowserView
from authentik.core.api.applications import ApplicationViewSet
from authentik.core.api.authenticated_sessions import AuthenticatedSessionViewSet
from authentik.core.api.devices import DeviceViewSet
from authentik.core.api.groups import GroupViewSet
from authentik.core.api.propertymappings import PropertyMappingViewSet
from authentik.core.api.providers import ProviderViewSet
from authentik.core.api.sources import SourceViewSet
from authentik.core.api.sources import SourceViewSet, UserSourceConnectionViewSet
from authentik.core.api.tokens import TokenViewSet
from authentik.core.api.users import UserViewSet
from authentik.crypto.api import CertificateKeyPairViewSet
@ -136,6 +136,7 @@ router.register("events/transports", NotificationTransportViewSet)
router.register("events/rules", NotificationRuleViewSet)
router.register("sources/all", SourceViewSet)
router.register("sources/user_connections/all", UserSourceConnectionViewSet)
router.register("sources/user_connections/oauth", UserOAuthSourceConnectionViewSet)
router.register("sources/user_connections/plex", PlexSourceConnectionViewSet)
router.register("sources/ldap", LDAPSourceViewSet)
@ -169,6 +170,7 @@ router.register("propertymappings/saml", SAMLPropertyMappingViewSet)
router.register("propertymappings/scope", ScopeMappingViewSet)
router.register("propertymappings/notification", NotificationWebhookMappingViewSet)
router.register("authenticators/all", DeviceViewSet, basename="device")
router.register("authenticators/duo", DuoDeviceViewSet)
router.register("authenticators/sms", SMSDeviceViewSet)
router.register("authenticators/static", StaticDeviceViewSet)
@ -246,7 +248,6 @@ urlpatterns = (
FlowInspectorView.as_view(),
name="flow-inspector",
),
path("sentry/", SentryTunnelView.as_view(), name="sentry"),
path("schema/", cache_page(86400)(SpectacularAPIView.as_view()), name="schema"),
]
)

View File

@ -0,0 +1,36 @@
"""Authenticator Devices API Views"""
from django_otp import devices_for_user
from django_otp.models import Device
from drf_spectacular.utils import extend_schema
from rest_framework.fields import CharField, IntegerField, SerializerMethodField
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet
from authentik.core.api.utils import MetaNameSerializer
class DeviceSerializer(MetaNameSerializer):
"""Serializer for Duo authenticator devices"""
pk = IntegerField()
name = CharField()
type = SerializerMethodField()
def get_type(self, instance: Device) -> str:
"""Get type of device"""
return instance._meta.label
class DeviceViewSet(ViewSet):
"""Viewset for authenticator devices"""
serializer_class = DeviceSerializer
permission_classes = [IsAuthenticated]
@extend_schema(responses={200: DeviceSerializer(many=True)})
def list(self, request: Request) -> Response:
"""Get all devices for current user"""
devices = devices_for_user(request.user)
return Response(DeviceSerializer(devices, many=True).data)

View File

@ -42,6 +42,7 @@ class GroupSerializer(ModelSerializer):
users_obj = ListSerializer(
child=GroupMemberSerializer(), read_only=True, source="users", required=False
)
parent_name = CharField(source="parent.name", read_only=True)
class Meta:
@ -51,6 +52,7 @@ class GroupSerializer(ModelSerializer):
"name",
"is_superuser",
"parent",
"parent_name",
"users",
"attributes",
"users_obj",

View File

@ -1,18 +1,21 @@
"""Source API Views"""
from typing import Iterable
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
from rest_framework.decorators import action
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, SerializerMethodField
from rest_framework.viewsets import GenericViewSet
from structlog.stdlib import get_logger
from authentik.api.authorization import OwnerFilter, OwnerPermissions
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.models import Source
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UserSettingSerializer
from authentik.lib.utils.reflection import all_subclasses
from authentik.policies.engine import PolicyEngine
@ -113,3 +116,39 @@ class SourceViewSet(
LOGGER.warning(source_settings.errors)
matching_sources.append(source_settings.validated_data)
return Response(matching_sources)
class UserSourceConnectionSerializer(SourceSerializer):
"""OAuth Source Serializer"""
source = SourceSerializer(read_only=True)
class Meta:
model = UserSourceConnection
fields = [
"pk",
"user",
"source",
"created",
]
extra_kwargs = {
"user": {"read_only": True},
"created": {"read_only": True},
}
class UserSourceConnectionViewSet(
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
UsedByMixin,
mixins.ListModelMixin,
GenericViewSet,
):
"""User-source connection Viewset"""
queryset = UserSourceConnection.objects.all()
serializer_class = UserSourceConnectionSerializer
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["pk"]

View File

@ -45,6 +45,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import LinkSerializer, PassiveSerializer, is_dict
from authentik.core.middleware import SESSION_IMPERSONATE_ORIGINAL_USER, SESSION_IMPERSONATE_USER
from authentik.core.models import (
USER_ATTRIBUTE_CHANGE_EMAIL,
USER_ATTRIBUTE_CHANGE_USERNAME,
USER_ATTRIBUTE_SA,
USER_ATTRIBUTE_TOKEN_EXPIRING,
@ -122,6 +123,14 @@ class UserSelfSerializer(ModelSerializer):
"pk": group.pk,
}
def validate_email(self, email: str):
"""Check if the user is allowed to change their email"""
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_EMAIL, True):
return email
if email != self.instance.email:
raise ValidationError("Not allowed to change email.")
return email
def validate_username(self, username: str):
"""Check if the user is allowed to change their username"""
if self.instance.group_attributes().get(USER_ATTRIBUTE_CHANGE_USERNAME, True):
@ -320,13 +329,14 @@ class UserViewSet(UsedByMixin, ModelViewSet):
# pylint: disable=invalid-name
def me(self, request: Request) -> Response:
"""Get information about current user"""
serializer = SessionUserSerializer(data={"user": UserSelfSerializer(request.user).data})
serializer = SessionUserSerializer(
data={"user": UserSelfSerializer(instance=request.user).data}
)
if SESSION_IMPERSONATE_USER in request._request.session:
serializer.initial_data["original"] = UserSelfSerializer(
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
instance=request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
).data
serializer.is_valid()
return Response(serializer.data)
return Response(serializer.initial_data)
@extend_schema(request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)})
@action(
@ -346,9 +356,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
# since it caches the full object
if SESSION_IMPERSONATE_USER in request.session:
request.session[SESSION_IMPERSONATE_USER] = new_user
serializer = SessionUserSerializer(data={"user": data.data})
serializer.is_valid()
return Response(serializer.data)
return Response({"user": data.data})
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
@extend_schema(responses={200: UserMetricsSerializer(many=False)})

View File

@ -55,5 +55,5 @@ class TokenBackend(InbuiltBackend):
if not tokens.exists():
return None
token = tokens.first()
self.set_method("password", request, token=token)
self.set_method("token", request, token=token)
return token.user

View File

View File

@ -0,0 +1,15 @@
"""Output full config"""
from json import dumps
from django.core.management.base import BaseCommand, no_translations
from authentik.lib.config import CONFIG
class Command(BaseCommand): # pragma: no cover
"""Output full config"""
@no_translations
def handle(self, *args, **options):
"""Check permissions for all apps"""
print(dumps(CONFIG.raw, indent=4))

View File

@ -39,7 +39,8 @@ USER_ATTRIBUTE_DEBUG = "goauthentik.io/user/debug"
USER_ATTRIBUTE_SA = "goauthentik.io/user/service-account"
USER_ATTRIBUTE_SOURCES = "goauthentik.io/user/sources"
USER_ATTRIBUTE_TOKEN_EXPIRING = "goauthentik.io/user/token-expires" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username" # nosec
USER_ATTRIBUTE_CHANGE_USERNAME = "goauthentik.io/user/can-change-username"
USER_ATTRIBUTE_CHANGE_EMAIL = "goauthentik.io/user/can-change-email"
USER_ATTRIBUTE_CAN_OVERRIDE_IP = "goauthentik.io/user/override-ips"
GRAVATAR_URL = "https://secure.gravatar.com"
@ -80,6 +81,27 @@ class Group(models.Model):
)
attributes = models.JSONField(default=dict, blank=True)
def is_member(self, user: "User") -> bool:
"""Recursively check if `user` is member of us, or any parent."""
query = """
WITH RECURSIVE parents AS (
SELECT authentik_core_group.*, 0 AS relative_depth
FROM authentik_core_group
WHERE authentik_core_group.group_uuid = %s
UNION ALL
SELECT authentik_core_group.*, parents.relative_depth - 1
FROM authentik_core_group,parents
WHERE authentik_core_group.parent_id = parents.group_uuid
)
SELECT group_uuid
FROM parents
GROUP BY group_uuid;
"""
groups = Group.objects.raw(query, [self.group_uuid])
return user.ak_groups.filter(pk__in=[group.pk for group in groups]).exists()
def __str__(self):
return f"Group {self.name}"

View File

@ -6,6 +6,7 @@ from os import environ
from boto3.exceptions import Boto3Error
from botocore.exceptions import BotoCoreError, ClientError
from dbbackup.db.exceptions import CommandConnectorError
from django.conf import settings
from django.contrib.humanize.templatetags.humanize import naturaltime
from django.contrib.sessions.backends.cache import KEY_PREFIX
from django.core import management
@ -55,24 +56,25 @@ def clean_expired_models(self: MonitoredTask):
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages))
def should_backup() -> bool:
"""Check if we should be doing backups"""
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
return False
if not CONFIG.y_bool("postgresql.backup.enabled"):
return False
if settings.DEBUG:
return False
return True
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task()
def backup_database(self: MonitoredTask): # pragma: no cover
"""Database backup"""
self.result_timeout_hours = 25
if SERVICE_HOST_ENV_NAME in environ and not CONFIG.y("postgresql.s3_backup.bucket"):
LOGGER.info("Running in k8s and s3 backups are not configured, skipping")
self.set_status(
TaskResult(
TaskResultStatus.WARNING,
[
(
"Skipping backup as authentik is running in Kubernetes "
"without S3 backups configured."
),
],
)
)
if not should_backup():
self.set_status(TaskResult(TaskResultStatus.UNKNOWN, ["Backups are not configured."]))
return
try:
start = datetime.now()

View File

@ -0,0 +1,40 @@
"""group tests"""
from django.test.testcases import TestCase
from authentik.core.models import Group, User
class TestGroups(TestCase):
"""Test group membership"""
def test_group_membership_simple(self):
"""Test simple membership"""
user = User.objects.create(username="user")
user2 = User.objects.create(username="user2")
group = Group.objects.create(name="group")
group.users.add(user)
self.assertTrue(group.is_member(user))
self.assertFalse(group.is_member(user2))
def test_group_membership_parent(self):
"""Test parent membership"""
user = User.objects.create(username="user")
user2 = User.objects.create(username="user2")
first = Group.objects.create(name="first")
second = Group.objects.create(name="second", parent=first)
second.users.add(user)
self.assertTrue(first.is_member(user))
self.assertFalse(first.is_member(user2))
def test_group_membership_parent_extra(self):
"""Test parent membership"""
user = User.objects.create(username="user")
user2 = User.objects.create(username="user2")
first = Group.objects.create(name="first")
second = Group.objects.create(name="second", parent=first)
third = Group.objects.create(name="third", parent=second)
second.users.add(user)
self.assertTrue(first.is_member(user))
self.assertFalse(first.is_member(user2))
self.assertFalse(third.is_member(user))
self.assertFalse(third.is_member(user2))

View File

@ -2,7 +2,7 @@
from django.urls.base import reverse
from rest_framework.test import APITestCase
from authentik.core.models import USER_ATTRIBUTE_CHANGE_USERNAME, User
from authentik.core.models import USER_ATTRIBUTE_CHANGE_EMAIL, USER_ATTRIBUTE_CHANGE_USERNAME, User
from authentik.flows.models import Flow, FlowDesignation
from authentik.stages.email.models import EmailStage
from authentik.tenants.models import Tenant
@ -33,6 +33,16 @@ class TestUsersAPI(APITestCase):
)
self.assertEqual(response.status_code, 400)
def test_update_self_email_denied(self):
"""Test update_self"""
self.admin.attributes[USER_ATTRIBUTE_CHANGE_EMAIL] = False
self.admin.save()
self.client.force_login(self.admin)
response = self.client.put(
reverse("authentik_api:user-update-self"), data={"email": "foo", "name": "foo"}
)
self.assertEqual(response.status_code, 400)
def test_metrics(self):
"""Test user's metrics"""
self.client.force_login(self.admin)

View File

@ -141,7 +141,7 @@ class CertificateKeyPairFilter(FilterSet):
class CertificateKeyPairViewSet(UsedByMixin, ModelViewSet):
"""CertificateKeyPair Viewset"""
queryset = CertificateKeyPair.objects.all()
queryset = CertificateKeyPair.objects.exclude(managed__isnull=False)
serializer_class = CertificateKeyPairSerializer
filterset_class = CertificateKeyPairFilter

View File

@ -7,16 +7,25 @@ from django.core.exceptions import SuspiciousOperation
from django.db.models import Model
from django.db.models.signals import post_save, pre_delete
from django.http import HttpRequest, HttpResponse
from django_otp.plugins.otp_static.models import StaticToken
from guardian.models import UserObjectPermission
from authentik.core.middleware import LOCAL
from authentik.core.models import User
from authentik.core.models import AuthenticatedSession, User
from authentik.events.models import Event, EventAction, Notification
from authentik.events.signals import EventNewThread
from authentik.events.utils import model_to_dict
from authentik.lib.sentry import before_send
from authentik.lib.utils.errors import exception_to_string
IGNORED_MODELS = (
Event,
Notification,
UserObjectPermission,
AuthenticatedSession,
StaticToken,
)
class AuditMiddleware:
"""Register handlers for duration of request-response that log creation/update/deletion
@ -82,7 +91,7 @@ class AuditMiddleware:
user: User, request: HttpRequest, sender, instance: Model, created: bool, **_
):
"""Signal handler for all object's post_save"""
if isinstance(instance, (Event, Notification, UserObjectPermission)):
if isinstance(instance, IGNORED_MODELS):
return
action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED
@ -92,7 +101,7 @@ class AuditMiddleware:
# pylint: disable=unused-argument
def pre_delete_handler(user: User, request: HttpRequest, sender, instance: Model, **_):
"""Signal handler for all object's pre_delete"""
if isinstance(instance, (Event, Notification, UserObjectPermission)): # pragma: no cover
if isinstance(instance, IGNORED_MODELS): # pragma: no cover
return
EventNewThread(

View File

@ -98,7 +98,9 @@ def notification_transport(self: MonitoredTask, notification_pk: int, transport_
notification: Notification = Notification.objects.filter(pk=notification_pk).first()
if not notification:
return
transport: NotificationTransport = NotificationTransport.objects.get(pk=transport_pk)
transport = NotificationTransport.objects.filter(pk=transport_pk).first()
if not transport:
return
transport.send(notification)
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL))
except NotificationTransportError as exc:

View File

@ -1,6 +1,4 @@
"""Flow Stage API Views"""
from typing import Iterable
from django.urls.base import reverse
from drf_spectacular.utils import extend_schema
from rest_framework import mixins
@ -15,7 +13,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import MetaNameSerializer, TypeCreateSerializer
from authentik.core.types import UserSettingSerializer
from authentik.flows.api.flows import FlowSerializer
from authentik.flows.models import Stage
from authentik.flows.models import ConfigurableStage, Stage
from authentik.lib.utils.reflection import all_subclasses
LOGGER = get_logger()
@ -86,9 +84,11 @@ class StageViewSet(
@action(detail=False, pagination_class=None, filter_backends=[])
def user_settings(self, request: Request) -> Response:
"""Get all stages the user can configure"""
_all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses().order_by("name")
stages = []
for configurable_stage in all_subclasses(ConfigurableStage):
stages += list(configurable_stage.objects.all().order_by("name"))
matching_stages: list[dict] = []
for stage in _all_stages:
for stage in stages:
user_settings = stage.ui_user_settings
if not user_settings:
continue

View File

@ -545,6 +545,7 @@ class TestFlowExecutor(APITestCase):
"password_fields": False,
"primary_action": "Log in",
"sources": [],
"show_source_labels": False,
"user_fields": [UserFields.E_MAIL],
},
)

View File

@ -60,6 +60,7 @@ class TestFlowInspector(APITestCase):
"password_fields": False,
"primary_action": "Log in",
"sources": [],
"show_source_labels": False,
"user_fields": ["username"],
},
)

View File

@ -5,6 +5,16 @@ postgresql:
user: authentik
port: 5432
password: 'env://POSTGRES_PASSWORD'
backup:
enabled: true
s3_backup:
access_key: ""
secret_key: ""
bucket: ""
region: eu-central-1
host: ""
location: ""
insecure_skip_verify: false
web:
listen: 0.0.0.0:9000
@ -58,6 +68,7 @@ outposts:
cookie_domain: null
disable_update_check: false
disable_startup_analytics: false
avatars: env://AUTHENTIK_AUTHENTIK__AVATARS?gravatar
geoip: "./GeoLite2-City.mmdb"

View File

@ -13,6 +13,7 @@ from django.db import InternalError, OperationalError, ProgrammingError
from django.http.response import Http404
from django_redis.exceptions import ConnectionInterrupted
from docker.errors import DockerException
from h11 import LocalProtocolError
from ldap3.core.exceptions import LDAPException
from redis.exceptions import ConnectionError as RedisConnectionError
from redis.exceptions import RedisError, ResponseError
@ -72,6 +73,7 @@ def before_send(event: dict, hint: dict) -> Optional[dict]:
# websocket errors
ChannelFull,
WebSocketException,
LocalProtocolError,
# rest_framework error
APIException,
# celery errors

View File

@ -1,8 +1,13 @@
"""authentik lib reflection utilities"""
import os
from importlib import import_module
from pathlib import Path
from typing import Union
from django.conf import settings
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from authentik.lib.config import CONFIG
def all_subclasses(cls, sort=True):
@ -42,3 +47,16 @@ def get_apps():
for _app in apps.get_app_configs():
if _app.name.startswith("authentik"):
yield _app
def get_env() -> str:
"""Get environment in which authentik is currently running"""
if SERVICE_HOST_ENV_NAME in os.environ:
return "kubernetes"
if "CI" in os.environ:
return "ci"
if Path("/tmp/authentik-mode").exists(): # nosec
return "compose"
if CONFIG.y_bool("debug"):
return "dev"
return "custom"

View File

@ -2,6 +2,7 @@
from time import sleep
from django.conf import settings
from django.utils.text import slugify
from docker import DockerClient
from docker.errors import DockerException, NotFound
from docker.models.containers import Container
@ -28,6 +29,17 @@ class DockerController(BaseController):
except ServiceConnectionInvalid as exc:
raise ControllerException from exc
@property
def name(self) -> str:
"""Get the name of the object this reconciler manages"""
return (
self.outpost.config.object_naming_template
% {
"name": slugify(self.outpost.name),
"uuid": self.outpost.uuid.hex,
}
).lower()
def _get_labels(self) -> dict[str, str]:
return {
"io.goauthentik.outpost-uuid": self.outpost.pk.hex,
@ -102,15 +114,14 @@ class DockerController(BaseController):
return image
def _get_container(self) -> tuple[Container, bool]:
container_name = f"authentik-proxy-{self.outpost.uuid.hex}"
try:
return self.client.containers.get(container_name), False
return self.client.containers.get(self.name), False
except NotFound:
self.logger.info("(Re-)creating container...")
image_name = self.try_pull_image()
container_args = {
"image": image_name,
"name": container_name,
"name": self.name,
"detach": True,
"environment": self._get_env(),
"labels": self._get_labels(),
@ -131,12 +142,23 @@ class DockerController(BaseController):
True,
)
def _migrate_container_name(self):
"""Migrate 2021.9 to 2021.10+"""
old_name = f"authentik-proxy-{self.outpost.uuid.hex}"
try:
old_container: Container = self.client.containers.get(old_name)
old_container.kill()
old_container.remove()
except NotFound:
return
# pylint: disable=too-many-return-statements
def up(self, depth=1):
if self.outpost.managed == MANAGED_OUTPOST:
return None
if depth >= 10:
raise ControllerException("Giving up since we exceeded recursion limit.")
self._migrate_container_name()
try:
container, has_been_created = self._get_container()
if has_been_created:

View File

@ -65,14 +65,14 @@ class PolicyBinding(SerializerModel):
# This is quite an ugly hack to prevent pylint from trying
# to resolve authentik_core.models.Group
# as python import path
"authentik_core." + "Group",
"authentik_core.Group",
on_delete=models.CASCADE,
default=None,
null=True,
blank=True,
)
user = models.ForeignKey(
"authentik_core." + "User",
"authentik_core.User",
on_delete=models.CASCADE,
default=None,
null=True,
@ -96,7 +96,7 @@ class PolicyBinding(SerializerModel):
self.policy: Policy
return self.policy.passes(request)
if self.group:
return PolicyResult(self.group.users.filter(pk=request.user.pk).exists())
return PolicyResult(self.group.is_member(request.user))
if self.user:
return PolicyResult(request.user == self.user)
return PolicyResult(False)

View File

@ -1,5 +1,5 @@
"""LDAPProvider API Views"""
from rest_framework.fields import CharField
from rest_framework.fields import CharField, ListField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet
@ -11,6 +11,8 @@ from authentik.providers.ldap.models import LDAPProvider
class LDAPProviderSerializer(ProviderSerializer):
"""LDAPProvider Serializer"""
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
class Meta:
model = LDAPProvider
@ -21,6 +23,8 @@ class LDAPProviderSerializer(ProviderSerializer):
"tls_server_name",
"uid_start_number",
"gid_start_number",
"outpost_set",
"search_mode",
]
@ -65,6 +69,7 @@ class LDAPOutpostConfigSerializer(ModelSerializer):
"tls_server_name",
"uid_start_number",
"gid_start_number",
"search_mode",
]

View File

@ -0,0 +1,93 @@
# Generated by Django 3.2.8 on 2021-11-05 09:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
replaces = [
("authentik_providers_ldap", "0001_initial"),
("authentik_providers_ldap", "0002_ldapprovider_search_group"),
("authentik_providers_ldap", "0003_auto_20210713_1138"),
("authentik_providers_ldap", "0004_auto_20210713_2115"),
("authentik_providers_ldap", "0005_ldapprovider_search_mode"),
]
initial = True
dependencies = [
("authentik_core", "0019_source_managed"),
("authentik_crypto", "0002_create_self_signed_kp"),
]
operations = [
migrations.CreateModel(
name="LDAPProvider",
fields=[
(
"provider_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="authentik_core.provider",
),
),
(
"base_dn",
models.TextField(
default="DC=ldap,DC=goauthentik,DC=io",
help_text="DN under which objects are accessible.",
),
),
(
"search_group",
models.ForeignKey(
default=None,
help_text="Users in this group can do search queries. If not set, every user can execute search queries.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
to="authentik_core.group",
),
),
(
"certificate",
models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="authentik_crypto.certificatekeypair",
),
),
("tls_server_name", models.TextField(blank=True, default="")),
(
"gid_start_number",
models.IntegerField(
default=4000,
help_text="The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
),
),
(
"uid_start_number",
models.IntegerField(
default=2000,
help_text="The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
),
),
(
"search_mode",
models.TextField(
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
),
),
],
options={
"verbose_name": "LDAP Provider",
"verbose_name_plural": "LDAP Providers",
},
bases=("authentik_core.provider", models.Model),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 3.2.8 on 2021-11-05 09:40
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_providers_ldap", "0004_auto_20210713_2115"),
]
operations = [
migrations.AddField(
model_name="ldapprovider",
name="search_mode",
field=models.TextField(
choices=[("direct", "Direct"), ("cached", "Cached")], default="direct"
),
),
]

View File

@ -10,6 +10,13 @@ from authentik.crypto.models import CertificateKeyPair
from authentik.outposts.models import OutpostModel
class SearchModes(models.TextChoices):
"""Search modes"""
DIRECT = "direct"
CACHED = "cached"
class LDAPProvider(OutpostModel, Provider):
"""Allow applications to authenticate against authentik's users using LDAP."""
@ -59,6 +66,8 @@ class LDAPProvider(OutpostModel, Provider):
),
)
search_mode = models.TextField(default=SearchModes.DIRECT, choices=SearchModes.choices)
@property
def launch_url(self) -> Optional[str]:
"""LDAP never has a launch URL"""

View File

@ -448,7 +448,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
elif self.provider.sub_mode == SubModes.USER_USERNAME:
sub = user.username
elif self.provider.sub_mode == SubModes.USER_UPN:
sub = user.attributes["upn"]
sub = user.attributes.get("upn", user.uid)
else:
raise ValueError(
(

View File

@ -386,6 +386,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
def pre_permission_check(self):
"""Check prompt parameter before checking permission/authentication,
see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
# Quick sanity check at the beginning to prevent event spamming
if len(self.request.GET) < 1:
raise Http404
try:
self.params = OAuthAuthorizationParams.from_request(self.request)
except AuthorizeError as error:

View File

@ -36,6 +36,7 @@ class ProxyProviderSerializer(ProviderSerializer):
"""ProxyProvider Serializer"""
redirect_uris = CharField(read_only=True)
outpost_set = ListField(child=CharField(), read_only=True, source="outpost_set.all")
def validate(self, attrs) -> dict[Any, str]:
"""Check that internal_host is set when mode is Proxy"""
@ -74,6 +75,7 @@ class ProxyProviderSerializer(ProviderSerializer):
"redirect_uris",
"cookie_domain",
"token_validity",
"outpost_set",
]

View File

@ -138,7 +138,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
SCOPE_AK_PROXY,
]
)
self.property_mappings.set(scopes)
self.property_mappings.add(*list(scopes))
self.redirect_uris = _get_callback_url(self.external_host)
def __str__(self):

View File

@ -59,11 +59,13 @@ class AuthNRequestParser:
) -> AuthNRequest:
root = ElementTree.fromstring(decoded_xml)
# http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf
# `AssertionConsumerServiceURL` can be omitted, and we should fallback to the
# default ACS URL
if "AssertionConsumerServiceURL" not in root.attrib:
msg = "Missing 'AssertionConsumerServiceURL' attribute"
LOGGER.warning(msg)
raise CannotHandleAssertion(msg)
request_acs_url = root.attrib["AssertionConsumerServiceURL"]
request_acs_url = self.provider.acs_url.lower()
else:
request_acs_url = root.attrib["AssertionConsumerServiceURL"]
if self.provider.acs_url.lower() != request_acs_url.lower():
msg = (

View File

@ -0,0 +1,30 @@
"""authentik recovery create_admin_group"""
from django.core.management.base import BaseCommand
from django.utils.translation import gettext as _
from authentik.core.models import Group, User
class Command(BaseCommand):
"""Create admin group if the default group gets deleted"""
help = _("Create admin group if the default group gets deleted.")
def add_arguments(self, parser):
parser.add_argument("user", action="store", help="User to add to the admin group.")
def handle(self, *args, **options):
"""Create admin group if the default group gets deleted"""
username = options.get("user")
user = User.objects.filter(username=username).first()
if not user:
self.stderr.write(f"User '{username}' not found.")
return
group, _ = Group.objects.update_or_create(
name="authentik Admins",
defaults={
"is_superuser": True,
},
)
group.users.add(user)
self.stdout.write(f"User '{username}' successfully added to the group 'authentik Admins'.")

View File

@ -7,12 +7,9 @@ from django.urls import reverse
from django.utils.text import slugify
from django.utils.timezone import now
from django.utils.translation import gettext as _
from structlog.stdlib import get_logger
from authentik.core.models import Token, TokenIntents, User
LOGGER = get_logger()
class Command(BaseCommand):
"""Create Token used to recover access"""

View File

@ -14,13 +14,13 @@ import importlib
import logging
import os
import sys
from hashlib import sha512
from json import dumps
from tempfile import gettempdir
from time import time
import structlog
from celery.schedules import crontab
from kubernetes.config.incluster_config import SERVICE_HOST_ENV_NAME
from sentry_sdk import init as sentry_init
from sentry_sdk.api import set_tag
from sentry_sdk.integrations.celery import CeleryIntegration
@ -32,6 +32,8 @@ from authentik.core.middleware import structlog_add_request_id
from authentik.lib.config import CONFIG
from authentik.lib.logging import add_process_id
from authentik.lib.sentry import before_send
from authentik.lib.utils.http import get_http_session
from authentik.lib.utils.reflection import get_env
from authentik.stages.password import BACKEND_APP_PASSWORD, BACKEND_INBUILT, BACKEND_LDAP
@ -176,6 +178,7 @@ SPECTACULAR_SETTINGS = {
"FlowDesignationEnum": "authentik.flows.models.FlowDesignation",
"PolicyEngineMode": "authentik.policies.models.PolicyEngineMode",
"ProxyMode": "authentik.providers.proxy.models.ProxyMode",
"PromptTypeEnum": "authentik.stages.prompt.models.FieldTypes",
},
"ENUM_ADD_EXPLICIT_BLANK_NULL_CHOICE": False,
"POSTPROCESSING_HOOKS": [
@ -307,7 +310,7 @@ EMAIL_HOST = CONFIG.y("email.host")
EMAIL_PORT = int(CONFIG.y("email.port"))
EMAIL_HOST_USER = CONFIG.y("email.username")
EMAIL_HOST_PASSWORD = CONFIG.y("email.password")
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", True)
EMAIL_USE_TLS = CONFIG.y_bool("email.use_tls", False)
EMAIL_USE_SSL = CONFIG.y_bool("email.use_ssl", False)
EMAIL_TIMEOUT = int(CONFIG.y("email.timeout"))
DEFAULT_FROM_EMAIL = CONFIG.y("email.from")
@ -380,7 +383,8 @@ DBBACKUP_CONNECTOR_MAPPING = {
"django_prometheus.db.backends.postgresql": "dbbackup.db.postgresql.PgDumpConnector",
}
DBBACKUP_TMP_DIR = gettempdir() if DEBUG else "/tmp" # nosec
if CONFIG.y("postgresql.s3_backup"):
DBBACKUP_CLEANUP_KEEP = 30
if CONFIG.y("postgresql.s3_backup.bucket", "") != "":
DBBACKUP_STORAGE = "storages.backends.s3boto3.S3Boto3Storage"
DBBACKUP_STORAGE_OPTIONS = {
"access_key": CONFIG.y("postgresql.s3_backup.access_key"),
@ -399,6 +403,12 @@ if CONFIG.y("postgresql.s3_backup"):
# Sentry integration
SENTRY_DSN = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
# Default to empty string as that is what docker has
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
if build_hash == "":
build_hash = "tagged"
env = get_env()
_ERROR_REPORTING = CONFIG.y_bool("error_reporting.enabled", False)
if _ERROR_REPORTING:
# pylint: disable=abstract-class-instantiated
@ -415,18 +425,29 @@ if _ERROR_REPORTING:
environment=CONFIG.y("error_reporting.environment", "customer"),
send_default_pii=CONFIG.y_bool("error_reporting.send_pii", False),
)
# Default to empty string as that is what docker has
build_hash = os.environ.get(ENV_GIT_HASH_KEY, "")
if build_hash == "":
build_hash = "tagged"
set_tag("authentik.build_hash", build_hash)
set_tag("authentik.env", "kubernetes" if SERVICE_HOST_ENV_NAME in os.environ else "compose")
set_tag("authentik.env", env)
set_tag("authentik.component", "backend")
j_print(
"Error reporting is enabled",
env=CONFIG.y("error_reporting.environment", "customer"),
)
if not CONFIG.y_bool("disable_startup_analytics", False):
should_send = env not in ["dev", "ci"]
if should_send:
get_http_session().post(
"https://goauthentik.io/api/event",
json={
"domain": "authentik",
"name": "pageview",
"url": f"http://localhost/{env}",
"referrer": f"{__version__} ({build_hash})",
},
headers={
"User-Agent": sha512(SECRET_KEY.encode("ascii")).hexdigest()[:16],
"Content-Type": "text/plain",
},
)
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.1/howto/static-files/

View File

@ -82,6 +82,8 @@ class BaseLDAPSynchronizer:
value = mapping.evaluate(user=None, request=None, ldap=kwargs, dn=object_dn)
if value is None:
continue
if isinstance(value, (bytes)):
continue
object_field = mapping.object_field
if object_field.startswith("attributes."):
# Because returning a list might desired, we can't

View File

@ -40,6 +40,9 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
self._logger.debug("Creating group with attributes", **defaults)
if "name" not in defaults:
raise IntegrityError("Name was not set by propertymappings")
# Special check for `users` field, as this is an M2M relation, and cannot be sync'd
if "users" in defaults:
del defaults["users"]
ak_group, created = Group.objects.update_or_create(
**{
f"attributes__{LDAP_UNIQUENESS}": uniq,

View File

@ -1,6 +1,4 @@
"""LDAP Sync tasks"""
from typing import Optional
from django.utils.text import slugify
from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger
@ -31,8 +29,7 @@ def ldap_sync_all():
@CELERY_APP.task(
bind=True, base=MonitoredTask, soft_time_limit=60 * 60 * 2, task_time_limit=60 * 60 * 2
)
# TODO: remove Optional[str] in 2021.10
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: Optional[str] = None):
def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
"""Synchronization of an LDAP Source"""
self.result_timeout_hours = 2
try:
@ -41,8 +38,6 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: Optional[str] = N
# Because the source couldn't be found, we don't have a UID
# to set the state with
return
if not sync_class:
return
sync = path_to_class(sync_class)
self.set_uid(f"{slugify(source.name)}-{sync.__name__}")
try:

View File

@ -21,6 +21,9 @@ class UserOAuthSourceConnectionSerializer(SourceSerializer):
"source",
"identifier",
]
extra_kwargs = {
"user": {"read_only": True},
}
class UserOAuthSourceConnectionViewSet(

View File

@ -12,6 +12,7 @@ class DiscordOAuthRedirect(OAuthRedirect):
def get_additional_parameters(self, source): # pragma: no cover
return {
"scope": "email identify",
"prompt": "none",
}

View File

@ -5,7 +5,7 @@ from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_sche
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.fields import CharField
from rest_framework.permissions import AllowAny
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.serializers import ValidationError
@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.utils import PassiveSerializer
from authentik.flows.challenge import RedirectChallenge
from authentik.flows.views.executor import to_stage_response
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.models import PlexSource, PlexSourceConnection
from authentik.sources.plex.plex import PlexAuth, PlexSourceFlowManager
LOGGER = get_logger()
@ -98,21 +98,11 @@ class PlexSourceViewSet(UsedByMixin, ModelViewSet):
user_info, identifier = auth_api.get_user_info()
# Check friendship first, then check server overlay
friends_allowed = False
owner_id = None
if source.allow_friends:
owner_api = PlexAuth(source, source.plex_token)
owner_id = owner_api.get_user_info
owner_friends = owner_api.get_friends()
for friend in owner_friends:
if int(friend.get("id", "0")) == int(identifier):
friends_allowed = True
LOGGER.info(
"allowing user for plex because of friend",
user=user_info["username"],
)
friends_allowed = owner_api.check_friends_overlap(identifier)
servers_allowed = auth_api.check_server_overlap()
owner_allowed = owner_id == identifier
if any([friends_allowed, servers_allowed, owner_allowed]):
if any([friends_allowed, servers_allowed]):
sfm = PlexSourceFlowManager(
source=source,
request=request,
@ -125,3 +115,57 @@ class PlexSourceViewSet(UsedByMixin, ModelViewSet):
user=user_info["username"],
)
raise PermissionDenied("Access denied.")
@extend_schema(
request=PlexTokenRedeemSerializer(),
responses={
204: OpenApiResponse(),
400: OpenApiResponse(description="Token not found"),
403: OpenApiResponse(description="Access denied"),
},
parameters=[
OpenApiParameter(
name="slug",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.STR,
)
],
)
@action(
methods=["POST"],
detail=False,
pagination_class=None,
filter_backends=[],
permission_classes=[IsAuthenticated],
)
def redeem_token_authenticated(self, request: Request) -> Response:
"""Redeem a plex token for an authenticated user, creating a connection"""
source: PlexSource = get_object_or_404(
PlexSource, slug=request.query_params.get("slug", "")
)
plex_token = request.data.get("plex_token", None)
if not plex_token:
raise ValidationError("No plex token given")
auth_api = PlexAuth(source, plex_token)
user_info, identifier = auth_api.get_user_info()
# Check friendship first, then check server overlay
friends_allowed = False
if source.allow_friends:
owner_api = PlexAuth(source, source.plex_token)
friends_allowed = owner_api.check_friends_overlap(identifier)
servers_allowed = auth_api.check_server_overlap()
if any([friends_allowed, servers_allowed]):
PlexSourceConnection.objects.create(
plex_token=plex_token,
user=request.user,
identifier=identifier,
source=source,
)
return Response(status=204)
LOGGER.warning(
"Denying plex connection because no server overlay and no friends and not owner",
user=user_info["username"],
friends_allowed=friends_allowed,
servers_allowed=servers_allowed,
)
raise PermissionDenied("Access denied.")

View File

@ -22,6 +22,9 @@ class PlexSourceConnectionSerializer(SourceSerializer):
"identifier",
"plex_token",
]
extra_kwargs = {
"user": {"read_only": True},
}
class PlexSourceConnectionViewSet(
@ -39,3 +42,4 @@ class PlexSourceConnectionViewSet(
filterset_fields = ["source__slug"]
permission_classes = [OwnerPermissions]
filter_backends = [OwnerFilter, DjangoFilterBackend, OrderingFilter, SearchFilter]
ordering = ["pk"]

View File

@ -83,6 +83,7 @@ class PlexSource(Source):
data={
"title": f"Plex {self.name}",
"component": "ak-user-settings-source-plex",
"configure_url": self.client_id,
}
)

View File

@ -36,7 +36,7 @@ class PlexAuth:
return {
"X-Plex-Product": "authentik",
"X-Plex-Version": __version__,
"X-Plex-Device-Vendor": "BeryJu.org",
"X-Plex-Device-Vendor": "goauthentik.io",
}
def get_resources(self) -> list[dict]:
@ -96,6 +96,21 @@ class PlexAuth:
return True
return False
def check_friends_overlap(self, user_ident: int) -> bool:
"""Check if the user is a friend of the owner, or the owner themselves"""
friends_allowed = False
_, owner_id = self.get_user_info()
owner_friends = self.get_friends()
for friend in owner_friends:
if int(friend.get("id", "0")) == user_ident:
friends_allowed = True
LOGGER.info(
"allowing user for plex because of friend",
user=user_ident,
)
owner_allowed = owner_id == user_ident
return any([friends_allowed, owner_allowed])
class PlexSourceFlowManager(SourceFlowManager):
"""Flow manager for plex sources"""

View File

@ -3,6 +3,7 @@ from requests import RequestException
from authentik.events.models import Event, EventAction
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
from authentik.lib.utils.errors import exception_to_string
from authentik.root.celery import CELERY_APP
from authentik.sources.plex.models import PlexSource
from authentik.sources.plex.plex import PlexAuth
@ -31,7 +32,7 @@ def check_plex_token(self: MonitoredTask, source_slug: int):
self.set_status(
TaskResult(
TaskResultStatus.ERROR,
["Plex token is invalid/an error occurred:", str(exc)],
["Plex token is invalid/an error occurred:", exception_to_string(exc)],
)
)
Event.new(

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.8 on 2021-10-31 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_sms", "0001_squashed_0004_auto_20211014_0936"),
]
operations = [
migrations.AlterField(
model_name="authenticatorsmsstage",
name="from_number",
field=models.TextField(),
),
]

View File

@ -101,7 +101,7 @@ class AuthenticatorSMSStageView(ChallengeStageView):
stage: AuthenticatorSMSStage = self.executor.current_stage
if SESSION_SMS_DEVICE not in self.request.session:
device = SMSDevice(user=user, confirmed=False, stage=stage)
device = SMSDevice(user=user, confirmed=False, stage=stage, name="SMS Device")
device.generate_token(commit=False)
if phone_number := self._has_phone_number():
device.phone_number = phone_number

View File

@ -55,7 +55,7 @@ class AuthenticatorStaticStageView(ChallengeStageView):
stage: AuthenticatorStaticStage = self.executor.current_stage
if SESSION_STATIC_DEVICE not in self.request.session:
device = StaticDevice(user=user, confirmed=True)
device = StaticDevice(user=user, confirmed=True, name="Static Token")
tokens = []
for _ in range(0, stage.token_count):
tokens.append(StaticToken(device=device, token=StaticToken.random_token()))

View File

@ -81,7 +81,9 @@ class AuthenticatorTOTPStageView(ChallengeStageView):
stage: AuthenticatorTOTPStage = self.executor.current_stage
if SESSION_TOTP_DEVICE not in self.request.session:
device = TOTPDevice(user=user, confirmed=True, digits=stage.digits)
device = TOTPDevice(
user=user, confirmed=True, digits=stage.digits, name="TOTP Authenticator"
)
self.request.session[SESSION_TOTP_DEVICE] = device
return super().get(request, *args, **kwargs)

View File

@ -75,6 +75,7 @@ class AuthenticatorValidateStageTests(APITestCase):
},
"user_fields": ["username"],
"sources": [],
"show_source_labels": False,
},
)

View File

@ -136,6 +136,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
credential_id=bytes_to_base64url(webauthn_credential.credential_id),
sign_count=webauthn_credential.sign_count,
rp_id=get_rp_id(self.request),
name="WebAuthn Device",
)
else:
return self.executor.stage_invalid("Device with Credential ID already exists.")

View File

@ -20,6 +20,7 @@ class IdentificationStageSerializer(StageSerializer):
"enrollment_flow",
"recovery_flow",
"sources",
"show_source_labels",
]
@ -35,5 +36,6 @@ class IdentificationStageViewSet(UsedByMixin, ModelViewSet):
"show_matched_user",
"enrollment_flow",
"recovery_flow",
"show_source_labels",
]
ordering = ["name"]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.2.8 on 2021-10-31 16:44
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_identification", "0011_alter_identificationstage_user_fields"),
]
operations = [
migrations.AddField(
model_name="identificationstage",
name="show_source_labels",
field=models.BooleanField(default=False),
),
]

View File

@ -81,6 +81,7 @@ class IdentificationStage(Stage):
sources = models.ManyToManyField(
Source, default=list, help_text=_("Specify which sources should be shown.")
)
show_source_labels = models.BooleanField(default=False)
@property
def serializer(self) -> BaseSerializer:

View File

@ -1,5 +1,6 @@
"""Identification stage logic"""
from dataclasses import asdict
from random import SystemRandom
from time import sleep
from typing import Any, Optional
@ -15,10 +16,16 @@ from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer
from authentik.core.models import Application, Source, User
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.flows.challenge import (
Challenge,
ChallengeResponse,
ChallengeTypes,
RedirectChallenge,
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import PLAN_CONTEXT_PENDING_USER_IDENTIFIER, ChallengeStageView
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE, challenge_types
from authentik.flows.views.executor import SESSION_KEY_APPLICATION_PRE
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
from authentik.stages.password.stage import authenticate
@ -28,8 +35,11 @@ LOGGER = get_logger()
@extend_schema_field(
PolymorphicProxySerializer(
component_name="ChallengeTypes",
serializers=challenge_types(),
component_name="LoginChallengeTypes",
serializers={
RedirectChallenge().fields["component"].default: RedirectChallenge,
PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
},
resource_type_field_name="component",
)
)
@ -57,6 +67,7 @@ class IdentificationChallenge(Challenge):
recovery_url = CharField(required=False)
primary_action = CharField()
sources = LoginSourceSerializer(many=True, required=False)
show_source_labels = BooleanField()
component = CharField(default="ak-stage-identification")
@ -77,7 +88,8 @@ class IdentificationChallengeResponse(ChallengeResponse):
pre_user = self.stage.get_user(uid_field)
if not pre_user:
sleep(0.150)
# Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
sleep(0.30 * SystemRandom().randint(3, 7))
LOGGER.debug("invalid_login", identifier=uid_field)
identification_failed.send(sender=self, request=self.stage.request, uid_field=uid_field)
# We set the pending_user even on failure so it's part of the context, even
@ -152,6 +164,7 @@ class IdentificationStageView(ChallengeStageView):
"component": "ak-stage-identification",
"user_fields": current_stage.user_fields,
"password_fields": bool(current_stage.password_stage),
"show_source_labels": current_stage.show_source_labels,
}
)
# If the user has been redirected to us whilst trying to access an

View File

@ -123,6 +123,7 @@ class TestIdentificationStage(APITestCase):
"name": "test",
}
],
"show_source_labels": False,
"user_fields": ["email"],
},
)
@ -158,6 +159,7 @@ class TestIdentificationStage(APITestCase):
{"code": "invalid", "string": "Failed to " "authenticate."}
]
},
"show_source_labels": False,
"flow_info": {
"background": self.flow.background_url,
"cancel_url": reverse("authentik_flows:cancel"),
@ -218,6 +220,7 @@ class TestIdentificationStage(APITestCase):
"authentik_core:if-flow",
kwargs={"flow_slug": "unique-enrollment-string"},
),
"show_source_labels": False,
"primary_action": "Log in",
"flow_info": {
"background": flow.background_url,
@ -267,6 +270,7 @@ class TestIdentificationStage(APITestCase):
"authentik_core:if-flow",
kwargs={"flow_slug": "unique-recovery-string"},
),
"show_source_labels": False,
"primary_action": "Log in",
"flow_info": {
"background": flow.background_url,

View File

@ -1,4 +1,6 @@
"""Invitation Stage API Views"""
from django_filters.filters import BooleanFilter
from django_filters.filterset import FilterSet
from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
@ -21,12 +23,23 @@ class InvitationStageSerializer(StageSerializer):
]
class InvitationStageFilter(FilterSet):
"""invitation filter"""
no_flows = BooleanFilter("flow", "isnull")
class Meta:
model = InvitationStage
fields = ["name", "no_flows", "continue_flow_without_invitation", "stage_uuid"]
class InvitationStageViewSet(UsedByMixin, ModelViewSet):
"""InvitationStage Viewset"""
queryset = InvitationStage.objects.all()
serializer_class = InvitationStageSerializer
filterset_fields = "__all__"
filterset_class = InvitationStageFilter
ordering = ["name"]
@ -53,7 +66,7 @@ class InvitationViewSet(UsedByMixin, ModelViewSet):
queryset = Invitation.objects.all()
serializer_class = InvitationSerializer
order = ["-expires"]
ordering = ["-expires"]
search_fields = ["created_by__username", "expires"]
filterset_fields = ["created_by__username", "expires"]

View File

@ -4,7 +4,6 @@ from typing import Optional
from deepmerge import always_merger
from django.http import HttpRequest, HttpResponse
from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from structlog.stdlib import get_logger
from authentik.flows.models import in_memory_stage
@ -50,7 +49,12 @@ class InvitationStageView(StageView):
return self.executor.stage_ok()
return self.executor.stage_invalid()
invite: Invitation = get_object_or_404(Invitation, pk=token)
invite: Invitation = Invitation.objects.filter(pk=token).first()
if not invite:
LOGGER.debug("invalid invitation", token=token)
if stage.continue_flow_without_invitation:
return self.executor.stage_ok()
return self.executor.stage_invalid()
self.executor.plan.context[INVITATION_IN_EFFECT] = True
self.executor.plan.context[INVITATION] = invite
@ -79,7 +83,9 @@ class InvitationFinalStageView(StageView):
if not invitation:
LOGGER.warning("InvitationFinalStageView stage called without invitation")
return HttpResponseBadRequest
if not invitation.single_use:
return self.executor.stage_ok()
invitation.delete()
token = invitation.invite_uuid.hex
if invitation.single_use:
invitation.delete()
LOGGER.debug("Deleted invitation", token=token)
del self.executor.plan.context[INVITATION]
return self.executor.stage_ok()

View File

@ -1,5 +1,5 @@
"""prompt models"""
from typing import Type
from typing import Any, Optional, Type
from uuid import uuid4
from django.db import models
@ -13,6 +13,7 @@ from rest_framework.fields import (
EmailField,
HiddenField,
IntegerField,
ReadOnlyField,
)
from rest_framework.serializers import BaseSerializer
@ -26,6 +27,10 @@ class FieldTypes(models.TextChoices):
# Simple text field
TEXT = "text", _("Text: Simple Text input")
# Simple text field
TEXT_READ_ONLY = "text_read_only", _(
"Text (read-only): Simple Text input, but cannot be edited."
)
# Same as text, but has autocomplete for password managers
USERNAME = (
"username",
@ -74,13 +79,15 @@ class Prompt(SerializerModel):
return PromptSerializer
@property
def field(self) -> CharField:
def field(self, default: Optional[Any]) -> CharField:
"""Get field type for Challenge and response"""
field_class = CharField
kwargs = {
"required": self.required,
}
if self.type == FieldTypes.TEXT_READ_ONLY:
field_class = ReadOnlyField
if self.type == FieldTypes.EMAIL:
field_class = EmailField
if self.type == FieldTypes.NUMBER:
@ -97,12 +104,14 @@ class Prompt(SerializerModel):
if self.type == FieldTypes.DATE_TIME:
field_class = DateTimeField
if self.type == FieldTypes.STATIC:
kwargs["initial"] = self.placeholder
kwargs["default"] = self.placeholder
kwargs["required"] = False
kwargs["label"] = ""
if self.type == FieldTypes.SEPARATOR:
kwargs["required"] = False
kwargs["label"] = ""
if default:
kwargs["default"] = default
return field_class(**kwargs)
def save(self, *args, **kwargs):

View File

@ -8,7 +8,7 @@ from django.http import HttpRequest, HttpResponse
from django.http.request import QueryDict
from django.utils.translation import gettext_lazy as _
from guardian.shortcuts import get_anonymous_user
from rest_framework.fields import BooleanField, CharField, IntegerField
from rest_framework.fields import BooleanField, CharField, ChoiceField, IntegerField
from rest_framework.serializers import ValidationError
from structlog.stdlib import get_logger
@ -31,7 +31,7 @@ class StagePromptSerializer(PassiveSerializer):
field_key = CharField()
label = CharField(allow_blank=True)
type = CharField()
type = ChoiceField(choices=FieldTypes.choices)
required = BooleanField()
placeholder = CharField(allow_blank=True)
order = IntegerField()
@ -65,7 +65,8 @@ class PromptChallengeResponse(ChallengeResponse):
fields = list(self.stage.fields.all())
for field in fields:
field: Prompt
self.fields[field.field_key] = field.field
current = plan.context.get(PLAN_CONTEXT_PROMPT, {}).get(field.field_key)
self.fields[field.field_key] = field.field(current)
# Special handling for fields with username type
# these check for existing users with the same username
if field.type == FieldTypes.USERNAME:
@ -96,10 +97,11 @@ class PromptChallengeResponse(ChallengeResponse):
# Check if we have any static or hidden fields, and ensure they
# still have the same value
static_hidden_fields: QuerySet[Prompt] = self.stage.fields.filter(
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC]
type__in=[FieldTypes.HIDDEN, FieldTypes.STATIC, FieldTypes.TEXT_READ_ONLY]
)
for static_hidden in static_hidden_fields:
attrs[static_hidden.field_key] = static_hidden.placeholder
field = self.fields[static_hidden.field_key]
attrs[static_hidden.field_key] = field.default
# Check if we have two password fields, and make sure they are the same
password_fields: QuerySet[Prompt] = self.stage.fields.filter(type=FieldTypes.PASSWORD)
@ -163,10 +165,17 @@ class PromptStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
fields = list(self.executor.current_stage.fields.all().order_by("order"))
serializers = []
context_prompt = self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
for field in fields:
data = StagePromptSerializer(field).data
if field.field_key in context_prompt:
data["placeholder"] = context_prompt.get(field.field_key)
serializers.append(data)
challenge = PromptChallenge(
data={
"type": ChallengeTypes.NATIVE.value,
"fields": [StagePromptSerializer(field).data for field in fields],
"fields": serializers,
},
)
return challenge

View File

@ -21,31 +21,32 @@ var running = true
func main() {
log.SetLevel(log.DebugLevel)
log.SetFormatter(&log.JSONFormatter{})
l := log.WithField("logger", "authentik.root")
config.DefaultConfig()
err := config.LoadConfig("./authentik/lib/default.yml")
if err != nil {
log.WithError(err).Warning("failed to load default config")
l.WithError(err).Warning("failed to load default config")
}
err = config.LoadConfig("./local.env.yml")
if err != nil {
log.WithError(err).Debug("no local config to load")
l.WithError(err).Debug("no local config to load")
}
err = config.FromEnv()
if err != nil {
log.WithError(err).Debug("failed to environment variables")
l.WithError(err).Debug("failed to environment variables")
}
config.ConfigureLogger()
if config.G.ErrorReporting.Enabled {
err := sentry.Init(sentry.ClientOptions{
Dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
Dsn: config.G.ErrorReporting.DSN,
AttachStacktrace: true,
TracesSampleRate: 0.6,
Release: fmt.Sprintf("authentik@%s", constants.VERSION),
Environment: config.G.ErrorReporting.Environment,
})
if err != nil {
log.WithError(err).Warning("failed to init sentry")
l.WithError(err).Warning("failed to init sentry")
}
}
@ -56,31 +57,31 @@ func main() {
g := gounicorn.NewGoUnicorn()
ws := web.NewWebServer(g)
defer g.Kill()
defer ws.Shutdown()
g.HealthyCallback = func() {
if !config.G.Web.DisableEmbeddedOutpost {
go attemptProxyStart(ws, u)
}
}
go web.RunMetricsServer()
for {
go attemptStartBackend(g)
ws.Start()
if !config.G.Web.DisableEmbeddedOutpost {
go attemptProxyStart(ws, u)
}
<-ex
running = false
log.WithField("logger", "authentik").Info("shutting down webserver")
l.WithField("logger", "authentik").Info("shutting down gunicorn")
go g.Kill()
l.WithField("logger", "authentik").Info("shutting down webserver")
go ws.Shutdown()
log.WithField("logger", "authentik").Info("killing gunicorn")
g.Kill()
}
}
func attemptStartBackend(g *gounicorn.GoUnicorn) {
for {
err := g.Start()
if !running {
return
}
err := g.Start()
log.WithField("logger", "authentik.router").WithError(err).Warning("gunicorn process died, restarting")
}
}
@ -88,8 +89,6 @@ func attemptStartBackend(g *gounicorn.GoUnicorn) {
func attemptProxyStart(ws *web.WebServer, u *url.URL) {
maxTries := 100
attempt := 0
// Sleep to wait for the app server to start
time.Sleep(30 * time.Second)
for {
log.WithField("logger", "authentik").Debug("attempting to init outpost")
ac := ak.NewAPIController(*u, config.G.SecretKey)

View File

@ -17,7 +17,7 @@ services:
image: redis:alpine
restart: unless-stopped
server:
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.1-rc2}
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.3}
restart: unless-stopped
command: server
environment:
@ -38,7 +38,7 @@ services:
- "0.0.0.0:9000:9000"
- "0.0.0.0:9443:9443"
worker:
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.1-rc2}
image: ${AUTHENTIK_IMAGE:-goauthentik.io/server}:${AUTHENTIK_TAG:-2021.10.3}
restart: unless-stopped
command: worker
environment:

13
go.mod
View File

@ -9,12 +9,8 @@ require (
github.com/garyburd/redigo v1.6.2 // indirect
github.com/getsentry/sentry-go v0.11.0
github.com/go-ldap/ldap/v3 v3.4.1
github.com/go-openapi/analysis v0.20.1 // indirect
github.com/go-openapi/errors v0.20.0 // indirect
github.com/go-openapi/runtime v0.20.0
github.com/go-openapi/strfmt v0.20.3
github.com/go-openapi/swag v0.19.15 // indirect
github.com/go-openapi/validate v0.20.2 // indirect
github.com/go-openapi/runtime v0.21.0
github.com/go-openapi/strfmt v0.21.0
github.com/golang-jwt/jwt v3.2.2+incompatible
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.3.0
@ -26,15 +22,14 @@ require (
github.com/imdario/mergo v0.3.12
github.com/mailru/easyjson v0.7.7 // indirect
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba
github.com/pires/go-proxyproto v0.6.1
github.com/pkg/errors v0.9.1
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
github.com/prometheus/client_golang v1.11.0
github.com/recws-org/recws v1.3.1
github.com/sirupsen/logrus v1.8.1
go.mongodb.org/mongo-driver v1.5.2 // indirect
goauthentik.io/api v0.2021101.2
goauthentik.io/api v0.2021102.6
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 // indirect
golang.org/x/net v0.0.0-20210510120150-4163338589ed // indirect
golang.org/x/oauth2 v0.0.0-20210323180902-22b0adad7558

44
go.sum
View File

@ -150,8 +150,8 @@ github.com/go-openapi/errors v0.19.6/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpX
github.com/go-openapi/errors v0.19.7/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.0 h1:Sxpo9PjEHDzhs3FbnGNonvDgWcMW2U7wGTcDDSFSceM=
github.com/go-openapi/errors v0.20.0/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.1 h1:j23mMDtRxMwIobkpId7sWh7Ddcx4ivaoqUbfXx5P+a8=
github.com/go-openapi/errors v0.20.1/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.18.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M=
github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg=
@ -162,8 +162,9 @@ github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3Hfo
github.com/go-openapi/jsonreference v0.18.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I=
github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc=
github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8=
github.com/go-openapi/jsonreference v0.19.5 h1:1WJP/wi4OjB4iV8KVbH73rQaoialJrqv8gitZLxGLtM=
github.com/go-openapi/jsonreference v0.19.5/go.mod h1:RdybgQwPxbL4UEjuAruzK1x3nE69AqPYEJeo/TWfEeg=
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/loads v0.17.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.18.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
github.com/go-openapi/loads v0.19.0/go.mod h1:72tmFy5wsWx89uEVddd0RjRWPZm92WRLhf7AC+0+OOU=
@ -173,16 +174,17 @@ github.com/go-openapi/loads v0.19.5/go.mod h1:dswLCAdonkRufe/gSUC3gN8nTSaB9uaS2e
github.com/go-openapi/loads v0.19.6/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc=
github.com/go-openapi/loads v0.19.7/go.mod h1:brCsvE6j8mnbmGBh103PT/QLHfbyDxA4hsKvYBNEGVc=
github.com/go-openapi/loads v0.20.0/go.mod h1:2LhKquiE513rN5xC6Aan6lYOSddlL8Mp20AW9kpviM4=
github.com/go-openapi/loads v0.20.2 h1:z5p5Xf5wujMxS1y8aP+vxwW5qYT2zdJBbXKmQUG3lcc=
github.com/go-openapi/loads v0.20.2/go.mod h1:hTVUotJ+UonAMMZsvakEgmWKgtulweO9vYP2bQYKA/o=
github.com/go-openapi/loads v0.21.0 h1:jYtUO4wwP7psAweisP/MDoOpdzsYEESdoPcsWjHDR68=
github.com/go-openapi/loads v0.21.0/go.mod h1:rHYve9nZrQ4CJhyeIIFJINGCg1tQpx2yJrrNo8sf1ws=
github.com/go-openapi/runtime v0.0.0-20180920151709-4f900dc2ade9/go.mod h1:6v9a6LTXWQCdL8k1AO3cvqx5OtZY/Y9wKTgaoP6YRfA=
github.com/go-openapi/runtime v0.19.0/go.mod h1:OwNfisksmmaZse4+gpV3Ne9AyMOlP1lt4sK4FXt0O64=
github.com/go-openapi/runtime v0.19.4/go.mod h1:X277bwSUBxVlCYR3r7xgZZGKVvBd/29gLDlFGtJ8NL4=
github.com/go-openapi/runtime v0.19.15/go.mod h1:dhGWCTKRXlAfGnQG0ONViOZpjfg0m2gUt9nTQPQZuoo=
github.com/go-openapi/runtime v0.19.16/go.mod h1:5P9104EJgYcizotuXhEuUrzVc+j1RiSjahULvYmlv98=
github.com/go-openapi/runtime v0.19.24/go.mod h1:Lm9YGCeecBnUUkFTxPC4s1+lwrkJ0pthx8YvyjCfkgk=
github.com/go-openapi/runtime v0.20.0 h1:DEV4oYH28MqakaabtbxH0cjvlzFegi/15kfUVCfiZW0=
github.com/go-openapi/runtime v0.20.0/go.mod h1:2WnLRxMiOUWNN0UZskSkxW0+WXdfB1KmqRKCFH+ZWYk=
github.com/go-openapi/runtime v0.21.0 h1:giZ8eT26R+/rx6RX2MkYjZPY8vPYVKDhP/mOazrQHzM=
github.com/go-openapi/runtime v0.21.0/go.mod h1:aQg+kaIQEn+A2CRSY1TxbM8+sT9g2V3aLc1FbIAnbbs=
github.com/go-openapi/spec v0.17.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.18.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI=
github.com/go-openapi/spec v0.19.2/go.mod h1:sCxk3jxKgioEJikev4fgkNmwS+3kuYdJtcsZsD5zxMY=
@ -192,8 +194,9 @@ github.com/go-openapi/spec v0.19.8/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHK
github.com/go-openapi/spec v0.19.15/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
github.com/go-openapi/spec v0.20.0/go.mod h1:+81FIL1JwC5P3/Iuuozq3pPE9dXdIEGxFutcFKaVbmU=
github.com/go-openapi/spec v0.20.1/go.mod h1:93x7oh+d+FQsmsieroS4cmR3u0p/ywH649a3qwC9OsQ=
github.com/go-openapi/spec v0.20.3 h1:uH9RQ6vdyPSs2pSy9fL8QPspDF2AMIMPtmK5coSSjtQ=
github.com/go-openapi/spec v0.20.3/go.mod h1:gG4F8wdEDN+YPBMVnzE85Rbhf+Th2DTvA9nFPQ5AYEg=
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/strfmt v0.17.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.18.0/go.mod h1:P82hnJI0CXkErkXi8IKjPbNBM6lV6+5pLP5l494TcyU=
github.com/go-openapi/strfmt v0.19.0/go.mod h1:+uW+93UVvGGq2qGaZxdDeJqSAqBqBdl+ZPMF/cC8nDY=
@ -203,8 +206,9 @@ github.com/go-openapi/strfmt v0.19.4/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk
github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk=
github.com/go-openapi/strfmt v0.19.11/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
github.com/go-openapi/strfmt v0.20.0/go.mod h1:UukAYgTaQfqJuAFlNxxMWNvMYiwiXtLsF2VwmoFtbtc=
github.com/go-openapi/strfmt v0.20.3 h1:YVG4ZgPZ00km/lRHrIf7c6cKL5/4FAUtG2T9RxWAgDY=
github.com/go-openapi/strfmt v0.20.3/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
github.com/go-openapi/strfmt v0.20.2/go.mod h1:43urheQI9dNtE5lTZQfuFJvjYJKPrxicATpEfZwHUNk=
github.com/go-openapi/strfmt v0.21.0 h1:hX2qEZKmYks+t0hKeb4VTJpUm2UYsdL3+DCid5swxIs=
github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg=
github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.18.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg=
github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
@ -223,8 +227,8 @@ github.com/go-openapi/validate v0.19.10/go.mod h1:RKEZTUWDkxKQxN2jDT7ZnZi2bhZlbN
github.com/go-openapi/validate v0.19.12/go.mod h1:Rzou8hA/CBw8donlS6WNEUQupNvUZ0waH08tGe6kAQ4=
github.com/go-openapi/validate v0.19.15/go.mod h1:tbn/fdOwYHgrhPBzidZfJC2MIVvs9GA7monOmWBbeCI=
github.com/go-openapi/validate v0.20.1/go.mod h1:b60iJT+xNNLfaQJUqLI7946tYiFEOuE9E4k54HpKcJ0=
github.com/go-openapi/validate v0.20.2 h1:AhqDegYV3J3iQkMPJSXkvzymHKMTw0BST3RK3hTT4ts=
github.com/go-openapi/validate v0.20.2/go.mod h1:e7OJoKNgd0twXZwIn0A43tHbvIcr/rZIVCbJBpTUoY0=
github.com/go-openapi/validate v0.20.3 h1:GZPPhhKSZrE8HjB4eEkoYAZmoWA4+tCemSgINH1/vKw=
github.com/go-openapi/validate v0.20.3/go.mod h1:goDdqVGiigM3jChcrYJxD2joalke3ZXeftD16byIjA4=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -375,6 +379,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.5/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.9.7/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -431,8 +436,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWb
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484 h1:D9EvfGQvlkKaDr2CRKN++7HbSXbefUNDrPq60T+g24s=
github.com/nmcclain/asn1-ber v0.0.0-20170104154839-2661553a0484/go.mod h1:O1EljZ+oHprtxDDPHiMWVo/5dBT6PlvWX5PSwj80aBA=
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3 h1:NNis9uuNpG5h97Dvxxo53Scg02qBg+3Nfabg6zjFGu8=
github.com/nmcclain/ldap v0.0.0-20191021200707-3b3b69a7e9e3/go.mod h1:YtrVB1/v9Td9SyjXpjYVmbdKgj9B0nPTBsdGUxy0i8U=
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba h1:DO8NFYdcRv1dnyAINJIBm6Bw2XibtLvQniNFGzf2W8E=
github.com/nmcclain/ldap v0.0.0-20210720162743-7f8d1e44eeba/go.mod h1:4S0XndRL8HNOaQBfdViJ2F/GPCgL524xlXRuXFH12/U=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
@ -547,15 +552,15 @@ go.mongodb.org/mongo-driver v1.4.3/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4S
go.mongodb.org/mongo-driver v1.4.4/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
go.mongodb.org/mongo-driver v1.4.6/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=
go.mongodb.org/mongo-driver v1.5.1/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
go.mongodb.org/mongo-driver v1.5.2 h1:AsxOLoJTgP6YNM0fXWw4OjdluYmWzQYp+lFJL7xu9fU=
go.mongodb.org/mongo-driver v1.5.2/go.mod h1:gRXCHX4Jo7J0IJ1oDQyUxF7jfy19UfxniMS4xxMmUqw=
go.mongodb.org/mongo-driver v1.7.3 h1:G4l/eYY9VrQAK/AUgkV0koQKzQnyddnWxrd/Etf0jIs=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
goauthentik.io/api v0.2021101.2 h1:MEmrcCmR/fWfxwVVlWPuVt4S3tigyv3OugNndLcFL3Y=
goauthentik.io/api v0.2021101.2/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
goauthentik.io/api v0.2021102.6 h1:jDah4AH28snsmFl9RwRMKrQ1ayW4zXrQWinryQdlJwA=
goauthentik.io/api v0.2021102.6/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@ -644,6 +649,7 @@ golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -709,6 +715,7 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -720,8 +727,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -26,6 +26,7 @@ func DefaultConfig() {
LogLevel: "info",
ErrorReporting: ErrorReportingConfig{
Enabled: false,
DSN: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8",
},
}
}

View File

@ -42,4 +42,5 @@ type ErrorReportingConfig struct {
Enabled bool `yaml:"enabled" env:"AUTHENTIK_ERROR_REPORTING__ENABLED"`
Environment string `yaml:"environment" env:"AUTHENTIK_ERROR_REPORTING__ENVIRONMENT"`
SendPII bool `yaml:"send_pii" env:"AUTHENTIK_ERROR_REPORTING__SEND_PII"`
DSN string
}

View File

@ -17,4 +17,4 @@ func OutpostUserAgent() string {
return fmt.Sprintf("authentik-outpost@%s (build=%s)", VERSION, BUILD())
}
const VERSION = "2021.10.1-rc2"
const VERSION = "2021.10.3"

View File

@ -4,6 +4,8 @@ import (
"net/http"
"os"
"os/exec"
"runtime"
"syscall"
"time"
log "github.com/sirupsen/logrus"
@ -11,6 +13,8 @@ import (
)
type GoUnicorn struct {
HealthyCallback func()
log *log.Entry
p *exec.Cmd
started bool
@ -21,10 +25,11 @@ type GoUnicorn struct {
func NewGoUnicorn() *GoUnicorn {
logger := log.WithField("logger", "authentik.router.unicorn")
g := &GoUnicorn{
log: logger,
started: false,
killed: false,
alive: false,
log: logger,
started: false,
killed: false,
alive: false,
HealthyCallback: func() {},
}
g.initCmd()
return g
@ -46,7 +51,7 @@ func (g *GoUnicorn) IsRunning() bool {
func (g *GoUnicorn) Start() error {
if g.killed {
g.log.Debug("Not restarting gunicorn since we're killed")
g.log.Debug("Not restarting gunicorn since we're shutdown")
return nil
}
if g.started {
@ -76,6 +81,7 @@ func (g *GoUnicorn) healthcheck() {
for range time.Tick(time.Second) {
if check() {
g.log.Info("backend is alive, backing off with healthchecks")
g.HealthyCallback()
break
}
g.log.Debug("backend not alive yet")
@ -87,8 +93,15 @@ func (g *GoUnicorn) healthcheck() {
func (g *GoUnicorn) Kill() {
g.killed = true
err := g.p.Process.Kill()
var err error
if runtime.GOOS == "darwin" {
g.log.WithField("method", "kill").Warning("stopping gunicorn")
err = g.p.Process.Kill()
} else {
g.log.WithField("method", "sigterm").Warning("stopping gunicorn")
err = syscall.Kill(g.p.Process.Pid, syscall.SIGTERM)
}
if err != nil {
g.log.WithError(err).Warning("failed to kill gunicorn")
g.log.WithError(err).Warning("failed to stop gunicorn")
}
}

View File

@ -35,7 +35,8 @@ type APIController struct {
logger *log.Entry
reloadOffset time.Duration
reloadOffset time.Duration
lastWsReconnect time.Time
wsConn *recws.RecConn
instanceUUID uuid.UUID
@ -142,6 +143,10 @@ func (a *APIController) StartBackgorundTasks() error {
"build": constants.BUILD(),
}).SetToCurrentTime()
}
go func() {
a.logger.Debug("Starting WS reconnector...")
a.startWSReConnector()
}()
go func() {
a.logger.Debug("Starting WS Handler...")
a.startWSHandler()

View File

@ -56,6 +56,7 @@ func (ac *APIController) initWS(akURL url.URL, outpostUUID strfmt.UUID) {
if err != nil {
ac.logger.WithField("logger", "authentik.outpost.ak-ws").WithError(err).Warning("Failed to hello to authentik")
}
ac.lastWsReconnect = time.Now()
}
// Shutdown Gracefully stops all workers, disconnects from websocket
@ -69,6 +70,20 @@ func (ac *APIController) Shutdown() {
}
}
func (ac *APIController) startWSReConnector() {
for {
time.Sleep(time.Second * 5)
if ac.wsConn.IsConnected() {
continue
}
if time.Since(ac.lastWsReconnect).Seconds() > 30 {
ac.wsConn.CloseAndReconnect()
ac.logger.Info("Reconnecting websocket")
ac.lastWsReconnect = time.Now()
}
}
}
func (ac *APIController) startWSHandler() {
logger := ac.logger.WithField("loop", "ws-handler")
for {
@ -80,8 +95,7 @@ func (ac *APIController) startWSHandler() {
"outpost_type": ac.Server.Type(),
"uuid": ac.instanceUUID.String(),
}).Set(0)
logger.WithError(err).Warning("ws write error, reconnecting")
ac.wsConn.CloseAndReconnect()
logger.WithError(err).Warning("ws read error")
time.Sleep(time.Second * 5)
continue
}
@ -126,8 +140,7 @@ func (ac *APIController) startWSHealth() {
err := ac.wsConn.WriteJSON(aliveMsg)
ac.logger.WithField("loop", "ws-health").Trace("hello'd")
if err != nil {
ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error, reconnecting")
ac.wsConn.CloseAndReconnect()
ac.logger.WithField("loop", "ws-health").WithError(err).Warning("ws write error")
time.Sleep(time.Second * 5)
continue
} else {

View File

@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/http/cookiejar"
"net/url"
@ -18,7 +17,6 @@ import (
"goauthentik.io/api"
"goauthentik.io/internal/constants"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/utils"
)
type StageComponent string
@ -103,8 +101,8 @@ type ChallengeInt interface {
GetResponseErrors() map[string][]api.ErrorDetail
}
func (fe *FlowExecutor) DelegateClientIP(a net.Addr) {
fe.cip = utils.GetIP(a)
func (fe *FlowExecutor) DelegateClientIP(a string) {
fe.cip = a
fe.api.GetConfig().AddDefaultHeader(HeaderAuthentikRemoteIP, fe.cip)
}

View File

@ -1,23 +0,0 @@
package ldap
import "crypto/tls"
func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
if len(ls.providers) == 1 {
if ls.providers[0].cert != nil {
ls.log.WithField("server-name", info.ServerName).Debug("We only have a single provider, using their cert")
return ls.providers[0].cert, nil
}
}
for _, provider := range ls.providers {
if provider.tlsServerName == &info.ServerName {
if provider.cert == nil {
ls.log.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate")
return ls.defaultCert, nil
}
return provider.cert, nil
}
}
ls.log.WithField("server-name", info.ServerName).Debug("Fallback to default cert")
return ls.defaultCert, nil
}

View File

@ -1,44 +1,18 @@
package ldap
import (
"context"
"net"
"strings"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
"github.com/nmcclain/ldap"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/outpost/ldap/bind"
"goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/utils"
)
type BindRequest struct {
BindDN string
BindPW string
id string
conn net.Conn
log *log.Entry
ctx context.Context
}
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
sentry.TransactionName("authentik.providers.ldap.bind"))
rid := uuid.New().String()
span.SetTag("request_uid", rid)
span.SetTag("user.username", bindDN)
req, span := bind.NewRequest(bindDN, bindPW, conn)
bindDN = strings.ToLower(bindDN)
req := BindRequest{
BindDN: bindDN,
BindPW: bindPW,
conn: conn,
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
id: rid,
ctx: span.Context(),
}
defer func() {
span.Finish()
metrics.Requests.With(prometheus.Labels{
@ -46,19 +20,19 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
"type": "bind",
"filter": "",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"client": req.RemoteAddr(),
}).Observe(float64(span.EndTime.Sub(span.StartTime)))
req.log.WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
req.Log().WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Bind request")
}()
for _, instance := range ls.providers {
username, err := instance.getUsername(bindDN)
username, err := instance.binder.GetUsername(bindDN)
if err == nil {
return instance.Bind(username, req)
return instance.binder.Bind(username, req)
} else {
req.log.WithError(err).Debug("Username not for instance")
req.Log().WithError(err).Debug("Username not for instance")
}
}
req.log.WithField("request", "bind").Warning("No provider found for request")
req.Log().WithField("request", "bind").Warning("No provider found for request")
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": ls.ac.Outpost.Name,
"type": "bind",
@ -68,10 +42,3 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
}).Inc()
return ldap.LDAPResultOperationsError, nil
}
func (ls *LDAPServer) TimerFlowCacheExpiry() {
for _, p := range ls.providers {
ls.log.WithField("flow", p.flowSlug).Debug("Pre-heating flow cache")
p.TimerFlowCacheExpiry()
}
}

View File

@ -0,0 +1,9 @@
package bind
import "github.com/nmcclain/ldap"
type Binder interface {
GetUsername(string) (string, error)
Bind(username string, req *Request) (ldap.LDAPResultCode, error)
TimerFlowCacheExpiry()
}

View File

@ -1,4 +1,4 @@
package ldap
package direct
import (
"context"
@ -12,14 +12,30 @@ import (
log "github.com/sirupsen/logrus"
"goauthentik.io/api"
"goauthentik.io/internal/outpost"
"goauthentik.io/internal/outpost/ldap/bind"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/utils"
"goauthentik.io/internal/outpost/ldap/server"
)
const ContextUserKey = "ak_user"
func (pi *ProviderInstance) getUsername(dn string) (string, error) {
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(pi.BaseDN)) {
type DirectBinder struct {
si server.LDAPServerInstance
log *log.Entry
}
func NewDirectBinder(si server.LDAPServerInstance) *DirectBinder {
db := &DirectBinder{
si: si,
log: log.WithField("logger", "authentik.outpost.ldap.binder.direct"),
}
db.log.Info("initialised direct binder")
return db
}
func (db *DirectBinder) GetUsername(dn string) (string, error) {
if !strings.HasSuffix(strings.ToLower(dn), strings.ToLower(db.si.GetBaseDN())) {
return "", errors.New("invalid base DN")
}
dns, err := goldap.ParseDN(dn)
@ -36,13 +52,13 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
return "", errors.New("failed to find cn")
}
func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) {
fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
func (db *DirectBinder) Bind(username string, req *bind.Request) (ldap.LDAPResultCode, error) {
fe := outpost.NewFlowExecutor(req.Context(), db.si.GetFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{
"bindDN": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"requestId": req.id,
"client": req.RemoteAddr(),
"requestId": req.ID(),
})
fe.DelegateClientIP(req.conn.RemoteAddr())
fe.DelegateClientIP(req.RemoteAddr())
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
fe.Answers[outpost.StageIdentification] = username
@ -51,83 +67,82 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
passed, err := fe.Execute()
if !passed {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "invalid_credentials",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"client": req.RemoteAddr(),
}).Inc()
return ldap.LDAPResultInvalidCredentials, nil
}
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "flow_error",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"client": req.RemoteAddr(),
}).Inc()
req.log.WithError(err).Warning("failed to execute flow")
req.Log().WithError(err).Warning("failed to execute flow")
return ldap.LDAPResultOperationsError, nil
}
access, err := fe.CheckApplicationAccess(pi.appSlug)
access, err := fe.CheckApplicationAccess(db.si.GetAppSlug())
if !access {
req.log.Info("Access denied for user")
req.Log().Info("Access denied for user")
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "access_denied",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"client": req.RemoteAddr(),
}).Inc()
return ldap.LDAPResultInsufficientAccessRights, nil
}
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "access_check_fail",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"client": req.RemoteAddr(),
}).Inc()
req.log.WithError(err).Warning("failed to check access")
req.Log().WithError(err).Warning("failed to check access")
return ldap.LDAPResultOperationsError, nil
}
req.log.Info("User has access")
uisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.bind.user_info")
req.Log().Info("User has access")
uisp := sentry.StartSpan(req.Context(), "authentik.providers.ldap.bind.user_info")
// Get user info to store in context
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"outpost_name": db.si.GetOutpostName(),
"type": "bind",
"reason": "user_info_fail",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
"client": req.RemoteAddr(),
}).Inc()
req.log.WithError(err).Warning("failed to get user info")
req.Log().WithError(err).Warning("failed to get user info")
return ldap.LDAPResultOperationsError, nil
}
pi.boundUsersMutex.Lock()
cs := pi.SearchAccessCheck(userInfo.User)
pi.boundUsers[req.BindDN] = UserFlags{
cs := db.SearchAccessCheck(userInfo.User)
flags := flags.UserFlags{
UserPk: userInfo.User.Pk,
CanSearch: cs != nil,
}
if pi.boundUsers[req.BindDN].CanSearch {
req.log.WithField("group", cs).Info("Allowed access to search")
db.si.SetFlags(req.BindDN, flags)
if flags.CanSearch {
req.Log().WithField("group", cs).Info("Allowed access to search")
}
uisp.Finish()
defer pi.boundUsersMutex.Unlock()
return ldap.LDAPResultSuccess, nil
}
// SearchAccessCheck Check if the current user is allowed to search
func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
func (db *DirectBinder) SearchAccessCheck(user api.UserSelf) *string {
for _, group := range user.Groups {
for _, allowedGroup := range pi.searchAllowedGroups {
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
for _, allowedGroup := range db.si.GetSearchAllowedGroups() {
db.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
if group.Pk == allowedGroup.String() {
return &group.Name
}
@ -136,13 +151,13 @@ func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
return nil
}
func (pi *ProviderInstance) TimerFlowCacheExpiry() {
fe := outpost.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{})
func (db *DirectBinder) TimerFlowCacheExpiry() {
fe := outpost.NewFlowExecutor(context.Background(), db.si.GetFlowSlug(), db.si.GetAPIClient().GetConfig(), log.Fields{})
fe.Params.Add("goauthentik.io/outpost/ldap", "true")
fe.Params.Add("goauthentik.io/outpost/ldap-warmup", "true")
err := fe.WarmUp()
if err != nil {
pi.log.WithError(err).Warning("failed to warm up flow cache")
db.log.WithError(err).Warning("failed to warm up flow cache")
}
}

View File

@ -0,0 +1,55 @@
package bind
import (
"context"
"net"
"strings"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
log "github.com/sirupsen/logrus"
"goauthentik.io/internal/utils"
)
type Request struct {
BindDN string
BindPW string
id string
conn net.Conn
log *log.Entry
ctx context.Context
}
func NewRequest(bindDN string, bindPW string, conn net.Conn) (*Request, *sentry.Span) {
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
sentry.TransactionName("authentik.providers.ldap.bind"))
rid := uuid.New().String()
span.SetTag("request_uid", rid)
span.SetTag("user.username", bindDN)
bindDN = strings.ToLower(bindDN)
return &Request{
BindDN: bindDN,
BindPW: bindPW,
conn: conn,
log: log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", utils.GetIP(conn.RemoteAddr())),
id: rid,
ctx: span.Context(),
}, span
}
func (r *Request) Context() context.Context {
return r.ctx
}
func (r *Request) Log() *log.Entry {
return r.log
}
func (r *Request) RemoteAddr() string {
return utils.GetIP(r.conn.RemoteAddr())
}
func (r *Request) ID() string {
return r.id
}

View File

@ -1,32 +0,0 @@
package ldap
import (
"net"
"time"
)
func (ls *LDAPServer) Close(boundDN string, conn net.Conn) error {
for _, p := range ls.providers {
p.delayDeleteUserInfo(boundDN)
}
return nil
}
func (pi *ProviderInstance) delayDeleteUserInfo(dn string) {
ticker := time.NewTicker(30 * time.Second)
quit := make(chan struct{})
go func() {
for {
select {
case <-ticker.C:
pi.boundUsersMutex.Lock()
delete(pi.boundUsers, dn)
pi.boundUsersMutex.Unlock()
close(quit)
case <-quit:
ticker.Stop()
return
}
}
}()
}

View File

@ -0,0 +1,21 @@
package constants
const (
OCGroup = "group"
OCGroupOfUniqueNames = "groupOfUniqueNames"
OCAKGroup = "goauthentik.io/ldap/group"
OCAKVirtualGroup = "goauthentik.io/ldap/virtual-group"
)
const (
OCUser = "user"
OCOrgPerson = "organizationalPerson"
OCInetOrgPerson = "inetOrgPerson"
OCAKUser = "goauthentik.io/ldap/user"
)
const (
OUUsers = "users"
OUGroups = "groups"
OUVirtualGroups = "virtual-groups"
)

View File

@ -0,0 +1,33 @@
package ldap
import (
"github.com/nmcclain/ldap"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/utils"
)
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
dn := pi.GetUserDN(u.Username)
attrs := utils.AKAttrsToLDAP(u.Attributes)
attrs = utils.EnsureAttributes(attrs, map[string][]string{
"memberOf": pi.GroupsForUser(u),
// Old fields for backwards compatibility
"accountStatus": {utils.BoolToString(*u.IsActive)},
"superuser": {utils.BoolToString(u.IsSuperuser)},
// End old fields
"goauthentik.io/ldap/active": {utils.BoolToString(*u.IsActive)},
"goauthentik.io/ldap/superuser": {utils.BoolToString(u.IsSuperuser)},
"cn": {u.Username},
"sAMAccountName": {u.Username},
"uid": {u.Uid},
"name": {u.Name},
"displayName": {u.Name},
"mail": {*u.Email},
"objectClass": {constants.OCUser, constants.OCOrgPerson, constants.OCInetOrgPerson, constants.OCAKUser},
"uidNumber": {pi.GetUidNumber(u)},
"gidNumber": {pi.GetUidNumber(u)},
})
return &ldap.Entry{DN: dn, Attributes: attrs}
}

View File

@ -0,0 +1,9 @@
package flags
import "goauthentik.io/api"
type UserFlags struct {
UserInfo *api.User
UserPk int32
CanSearch bool
}

View File

@ -0,0 +1,66 @@
package group
import (
"github.com/nmcclain/ldap"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/ldap/constants"
"goauthentik.io/internal/outpost/ldap/server"
"goauthentik.io/internal/outpost/ldap/utils"
)
type LDAPGroup struct {
DN string
CN string
Uid string
GidNumber string
Member []string
IsSuperuser bool
IsVirtualGroup bool
AKAttributes interface{}
}
func (lg *LDAPGroup) Entry() *ldap.Entry {
attrs := utils.AKAttrsToLDAP(lg.AKAttributes)
objectClass := []string{constants.OCGroup, constants.OCGroupOfUniqueNames, constants.OCAKGroup}
if lg.IsVirtualGroup {
objectClass = append(objectClass, constants.OCAKVirtualGroup)
}
attrs = utils.EnsureAttributes(attrs, map[string][]string{
"objectClass": objectClass,
"member": lg.Member,
"goauthentik.io/ldap/superuser": {utils.BoolToString(lg.IsSuperuser)},
"cn": {lg.CN},
"uid": {lg.Uid},
"sAMAccountName": {lg.CN},
"gidNumber": {lg.GidNumber},
})
return &ldap.Entry{DN: lg.DN, Attributes: attrs}
}
func FromAPIGroup(g api.Group, si server.LDAPServerInstance) *LDAPGroup {
return &LDAPGroup{
DN: si.GetGroupDN(g.Name),
CN: g.Name,
Uid: string(g.Pk),
GidNumber: si.GetGidNumber(g),
Member: si.UsersForGroup(g),
IsVirtualGroup: false,
IsSuperuser: *g.IsSuperuser,
AKAttributes: g.Attributes,
}
}
func FromAPIUser(u api.User, si server.LDAPServerInstance) *LDAPGroup {
return &LDAPGroup{
DN: si.GetVirtualGroupDN(u.Username),
CN: u.Username,
Uid: u.Uid,
GidNumber: si.GetUidNumber(u),
Member: []string{si.GetUserDN(u.Username)},
IsVirtualGroup: true,
IsSuperuser: false,
AKAttributes: nil,
}
}

View File

@ -0,0 +1,4 @@
package handler
type Handler interface {
}

View File

@ -0,0 +1,83 @@
package ldap
import (
"crypto/tls"
"sync"
"github.com/go-openapi/strfmt"
log "github.com/sirupsen/logrus"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/ldap/bind"
"goauthentik.io/internal/outpost/ldap/flags"
"goauthentik.io/internal/outpost/ldap/search"
)
type ProviderInstance struct {
BaseDN string
UserDN string
VirtualGroupDN string
GroupDN string
searcher search.Searcher
binder bind.Binder
appSlug string
flowSlug string
s *LDAPServer
log *log.Entry
tlsServerName *string
cert *tls.Certificate
outpostName string
searchAllowedGroups []*strfmt.UUID
boundUsersMutex sync.RWMutex
boundUsers map[string]flags.UserFlags
uidStartNumber int32
gidStartNumber int32
}
func (pi *ProviderInstance) GetAPIClient() *api.APIClient {
return pi.s.ac.Client
}
func (pi *ProviderInstance) GetBaseDN() string {
return pi.BaseDN
}
func (pi *ProviderInstance) GetBaseGroupDN() string {
return pi.GroupDN
}
func (pi *ProviderInstance) GetBaseUserDN() string {
return pi.UserDN
}
func (pi *ProviderInstance) GetOutpostName() string {
return pi.outpostName
}
func (pi *ProviderInstance) GetFlags(dn string) (flags.UserFlags, bool) {
pi.boundUsersMutex.RLock()
flags, ok := pi.boundUsers[dn]
pi.boundUsersMutex.RUnlock()
return flags, ok
}
func (pi *ProviderInstance) SetFlags(dn string, flag flags.UserFlags) {
pi.boundUsersMutex.Lock()
pi.boundUsers[dn] = flag
pi.boundUsersMutex.Unlock()
}
func (pi *ProviderInstance) GetAppSlug() string {
return pi.appSlug
}
func (pi *ProviderInstance) GetFlowSlug() string {
return pi.flowSlug
}
func (pi *ProviderInstance) GetSearchAllowedGroups() []*strfmt.UUID {
return pi.searchAllowedGroups
}

View File

@ -1,244 +0,0 @@
package ldap
import (
"errors"
"fmt"
"strings"
"sync"
"github.com/getsentry/sentry-go"
"github.com/nmcclain/ldap"
"github.com/prometheus/client_golang/prometheus"
"goauthentik.io/api"
"goauthentik.io/internal/outpost/ldap/metrics"
"goauthentik.io/internal/utils"
)
func (pi *ProviderInstance) SearchMe(req SearchRequest, f UserFlags) (ldap.ServerSearchResult, error) {
if f.UserInfo == nil {
u, _, err := pi.s.ac.Client.CoreApi.CoreUsersRetrieve(req.ctx, f.UserPk).Execute()
if err != nil {
req.log.WithError(err).Warning("Failed to get user info")
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("failed to get userinfo")
}
f.UserInfo = &u
}
entries := make([]*ldap.Entry, 1)
entries[0] = pi.UserEntry(*f.UserInfo)
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult, error) {
accsp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.check_access")
baseDN := strings.ToLower("," + pi.BaseDN)
entries := []*ldap.Entry{}
filterEntity, err := ldap.GetFilterObjectClass(req.Filter)
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"type": "search",
"reason": "filter_parse_fail",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
}
if len(req.BindDN) < 1 {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"type": "search",
"reason": "empty_bind_dn",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: Anonymous BindDN not allowed %s", req.BindDN)
}
if !strings.HasSuffix(req.BindDN, baseDN) {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"type": "search",
"reason": "invalid_bind_dn",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, fmt.Errorf("Search Error: BindDN %s not in our BaseDN %s", req.BindDN, pi.BaseDN)
}
pi.boundUsersMutex.RLock()
flags, ok := pi.boundUsers[req.BindDN]
pi.boundUsersMutex.RUnlock()
if !ok {
pi.log.Debug("User info not cached")
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"type": "search",
"reason": "user_info_not_cached",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
}
if req.SearchRequest.Scope == ldap.ScopeBaseObject {
pi.log.Debug("base scope, showing domain info")
return pi.SearchBase(req, flags.CanSearch)
}
if !flags.CanSearch {
pi.log.Debug("User can't search, showing info about user")
return pi.SearchMe(req, flags)
}
accsp.Finish()
parsedFilter, err := ldap.CompileFilter(req.Filter)
if err != nil {
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"type": "search",
"reason": "filter_parse_fail",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
}
// Create a custom client to set additional headers
c := api.NewAPIClient(pi.s.ac.Client.GetConfig())
c.GetConfig().AddDefaultHeader("X-authentik-outpost-ldap-query", req.Filter)
switch filterEntity {
default:
metrics.RequestsRejected.With(prometheus.Labels{
"outpost_name": pi.outpostName,
"type": "search",
"reason": "unhandled_filter_type",
"dn": req.BindDN,
"client": utils.GetIP(req.conn.RemoteAddr()),
}).Inc()
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
case "groupOfUniqueNames":
fallthrough
case "goauthentik.io/ldap/group":
fallthrough
case "goauthentik.io/ldap/virtual-group":
fallthrough
case GroupObjectClass:
wg := sync.WaitGroup{}
wg.Add(2)
gEntries := make([]*ldap.Entry, 0)
uEntries := make([]*ldap.Entry, 0)
go func() {
defer wg.Done()
gapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_group")
searchReq, skip := parseFilterForGroup(c.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter, false)
if skip {
pi.log.Trace("Skip backend request")
return
}
groups, _, err := searchReq.Execute()
gapisp.Finish()
if err != nil {
req.log.WithError(err).Warning("failed to get groups")
return
}
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
for _, g := range groups.Results {
gEntries = append(gEntries, pi.GroupEntry(pi.APIGroupToLDAPGroup(g)))
}
}()
go func() {
defer wg.Done()
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
searchReq, skip := parseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
if skip {
pi.log.Trace("Skip backend request")
return
}
users, _, err := searchReq.Execute()
uapisp.Finish()
if err != nil {
req.log.WithError(err).Warning("failed to get users")
return
}
for _, u := range users.Results {
uEntries = append(uEntries, pi.GroupEntry(pi.APIUserToLDAPGroup(u)))
}
}()
wg.Wait()
entries = append(gEntries, uEntries...)
case "":
fallthrough
case "organizationalPerson":
fallthrough
case "inetOrgPerson":
fallthrough
case "goauthentik.io/ldap/user":
fallthrough
case UserObjectClass:
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
searchReq, skip := parseFilterForUser(c.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter, false)
if skip {
pi.log.Trace("Skip backend request")
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
users, _, err := searchReq.Execute()
uapisp.Finish()
if err != nil {
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
}
for _, u := range users.Results {
entries = append(entries, pi.UserEntry(u))
}
}
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
}
func (pi *ProviderInstance) UserEntry(u api.User) *ldap.Entry {
dn := pi.GetUserDN(u.Username)
attrs := AKAttrsToLDAP(u.Attributes)
attrs = pi.ensureAttributes(attrs, map[string][]string{
"memberOf": pi.GroupsForUser(u),
// Old fields for backwards compatibility
"accountStatus": {BoolToString(*u.IsActive)},
"superuser": {BoolToString(u.IsSuperuser)},
"goauthentik.io/ldap/active": {BoolToString(*u.IsActive)},
"goauthentik.io/ldap/superuser": {BoolToString(u.IsSuperuser)},
"cn": {u.Username},
"sAMAccountName": {u.Username},
"uid": {u.Uid},
"name": {u.Name},
"displayName": {u.Name},
"mail": {*u.Email},
"objectClass": {UserObjectClass, "organizationalPerson", "inetOrgPerson", "goauthentik.io/ldap/user"},
"uidNumber": {pi.GetUidNumber(u)},
"gidNumber": {pi.GetUidNumber(u)},
})
return &ldap.Entry{DN: dn, Attributes: attrs}
}
func (pi *ProviderInstance) GroupEntry(g LDAPGroup) *ldap.Entry {
attrs := AKAttrsToLDAP(g.akAttributes)
objectClass := []string{GroupObjectClass, "groupOfUniqueNames", "goauthentik.io/ldap/group"}
if g.isVirtualGroup {
objectClass = append(objectClass, "goauthentik.io/ldap/virtual-group")
}
attrs = pi.ensureAttributes(attrs, map[string][]string{
"objectClass": objectClass,
"member": g.member,
"goauthentik.io/ldap/superuser": {BoolToString(g.isSuperuser)},
"cn": {g.cn},
"uid": {g.uid},
"sAMAccountName": {g.cn},
"gidNumber": {g.gidNumber},
})
return &ldap.Entry{DN: g.dn, Attributes: attrs}
}

View File

@ -2,50 +2,18 @@ package ldap
import (
"crypto/tls"
"net"
"sync"
"github.com/go-openapi/strfmt"
"github.com/pires/go-proxyproto"
log "github.com/sirupsen/logrus"
"goauthentik.io/api"
"goauthentik.io/internal/crypto"
"goauthentik.io/internal/outpost/ak"
"goauthentik.io/internal/outpost/ldap/metrics"
"github.com/nmcclain/ldap"
)
const GroupObjectClass = "group"
const UserObjectClass = "user"
type ProviderInstance struct {
BaseDN string
UserDN string
VirtualGroupDN string
GroupDN string
appSlug string
flowSlug string
s *LDAPServer
log *log.Entry
tlsServerName *string
cert *tls.Certificate
outpostName string
searchAllowedGroups []*strfmt.UUID
boundUsersMutex sync.RWMutex
boundUsers map[string]UserFlags
uidStartNumber int32
gidStartNumber int32
}
type UserFlags struct {
UserInfo *api.User
UserPk int32
CanSearch bool
}
type LDAPServer struct {
s *ldap.Server
log *log.Entry
@ -55,17 +23,6 @@ type LDAPServer struct {
providers []*ProviderInstance
}
type LDAPGroup struct {
dn string
cn string
uid string
gidNumber string
member []string
isSuperuser bool
isVirtualGroup bool
akAttributes interface{}
}
func NewServer(ac *ak.APIController) *LDAPServer {
s := ldap.NewServer()
s.EnforceLDAP = true
@ -83,10 +40,60 @@ func NewServer(ac *ak.APIController) *LDAPServer {
ls.defaultCert = &defaultCert
s.BindFunc("", ls)
s.SearchFunc("", ls)
s.CloseFunc("", ls)
return ls
}
func (ls *LDAPServer) Type() string {
return "ldap"
}
func (ls *LDAPServer) StartLDAPServer() error {
listen := "0.0.0.0:3389"
ln, err := net.Listen("tcp", listen)
if err != nil {
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
}
proxyListener := &proxyproto.Listener{Listener: ln}
defer proxyListener.Close()
ls.log.WithField("listen", listen).Info("Starting ldap server")
err = ls.s.Serve(proxyListener)
if err != nil {
return err
}
ls.log.Printf("closing %s", ln.Addr())
return ls.s.ListenAndServe(listen)
}
func (ls *LDAPServer) Start() error {
wg := sync.WaitGroup{}
wg.Add(3)
go func() {
defer wg.Done()
metrics.RunServer()
}()
go func() {
defer wg.Done()
err := ls.StartLDAPServer()
if err != nil {
panic(err)
}
}()
go func() {
defer wg.Done()
err := ls.StartLDAPTLSServer()
if err != nil {
panic(err)
}
}()
wg.Wait()
return nil
}
func (ls *LDAPServer) TimerFlowCacheExpiry() {
for _, p := range ls.providers {
ls.log.WithField("flow", p.flowSlug).Debug("Pre-heating flow cache")
p.binder.TimerFlowCacheExpiry()
}
}

View File

@ -0,0 +1,55 @@
package ldap
import (
"crypto/tls"
"net"
"github.com/pires/go-proxyproto"
)
func (ls *LDAPServer) getCertificates(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
if len(ls.providers) == 1 {
if ls.providers[0].cert != nil {
ls.log.WithField("server-name", info.ServerName).Debug("We only have a single provider, using their cert")
return ls.providers[0].cert, nil
}
}
for _, provider := range ls.providers {
if provider.tlsServerName == &info.ServerName {
if provider.cert == nil {
ls.log.WithField("server-name", info.ServerName).Debug("Handler does not have a certificate")
return ls.defaultCert, nil
}
return provider.cert, nil
}
}
ls.log.WithField("server-name", info.ServerName).Debug("Fallback to default cert")
return ls.defaultCert, nil
}
func (ls *LDAPServer) StartLDAPTLSServer() error {
listen := "0.0.0.0:6636"
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
MaxVersion: tls.VersionTLS12,
GetCertificate: ls.getCertificates,
}
ln, err := net.Listen("tcp", listen)
if err != nil {
ls.log.WithField("listen", listen).WithError(err).Fatalf("FATAL: listen failed")
}
proxyListener := &proxyproto.Listener{Listener: ln}
defer proxyListener.Close()
tln := tls.NewListener(proxyListener, tlsConfig)
ls.log.WithField("listen", listen).Info("Starting ldap tls server")
err = ls.s.Serve(tln)
if err != nil {
return err
}
ls.log.Printf("closing %s", ln.Addr())
return ls.s.ListenAndServe(listen)
}

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