Merge branch 'master' into version-2021.12

This commit is contained in:
Jens Langhammer 2021-12-15 10:16:05 +01:00
commit fbb6756488
125 changed files with 2887 additions and 1747 deletions

1
.github/stale.yml vendored
View file

@ -7,6 +7,7 @@ exemptLabels:
- pinned
- security
- pr_wanted
- enhancement/confirmed
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale because it has not had

1
.python-version Normal file
View file

@ -0,0 +1 @@
3.9.7

View file

@ -58,8 +58,6 @@ RUN apt-get update && \
curl ca-certificates gnupg git runit libpq-dev \
postgresql-client build-essential libxmlsec1-dev \
pkg-config libmaxminddb0 && \
pip install lxml==4.6.4 --no-cache-dir && \
export C_INCLUDE_PATH=/usr/local/lib/python3.10/site-packages/lxml/includes && \
pip install -r /requirements.txt --no-cache-dir && \
apt-get remove --purge -y build-essential git && \
apt-get autoremove --purge -y && \

View file

@ -4,7 +4,7 @@ UID = $(shell id -u)
GID = $(shell id -g)
NPM_VERSION = $(shell python -m scripts.npm_version)
all: lint-fix lint test gen
all: lint-fix lint test gen web
test-integration:
coverage run manage.py test tests/integration

View file

@ -32,15 +32,14 @@ geoip2 = "*"
gunicorn = "*"
kubernetes = "==v19.15.0"
ldap3 = "*"
# 4.7.0 and later remove `lxml-version.h` which is required by xmlsec
lxml = "==4.6.5"
lxml = "*"
packaging = "*"
psycopg2-binary = "*"
pycryptodome = "*"
pyjwt = "*"
pyyaml = "*"
requests-oauthlib = "*"
sentry-sdk = "*"
sentry-sdk = { git = 'https://github.com/beryju/sentry-python.git', ref = '379aee28b15d3b87b381317746c4efd24b3d7bc3' }
service_identity = "*"
structlog = "*"
swagger-spec-validator = "*"

156
Pipfile.lock generated
View file

@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "6a89870496296af32dbc2f64b0832d4c20010829ada0b3c4dc27fee56b68fad9"
"sha256": "dedb51159ef09fd9b00ab28022706f525c9df057ffd646e2a552784341a10538"
},
"pipfile-spec": 6,
"requires": {},
@ -169,19 +169,19 @@
},
"boto3": {
"hashes": [
"sha256:76b3ee0d1dd860c9218bc864cd29f1ee986f6e1e75e8669725dd3c411039379e",
"sha256:c39cb6ed376ba1d4689ac8f6759a2b2d8a0b0424dbec0cd3af1558079bcf06e8"
"sha256:739705b28e6b2329ea3b481ba801d439c296aaf176f7850729147ba99bbf8a9a",
"sha256:8f08e8e94bf107c5e9866684e9aadf8d9f60abed0cfe5c1dba4e7328674a1986"
],
"index": "pypi",
"version": "==1.20.23"
"version": "==1.20.24"
},
"botocore": {
"hashes": [
"sha256:640b62110aa6d1c25553eceafb5bcd89aedeb84b191598d1f6492ad24374d285",
"sha256:7459766c4594f3b8877e8013f93f0dc6c6486acbeb7d9c9ae488396529cc2e84"
"sha256:43006b4f52d7bb655319d3da0f615cdbee7762853acc1ebcb1d49f962e6b4806",
"sha256:e78d48c50c8c013fb9b362c6202fece2fe868edfd89b51968080180bdff41617"
],
"markers": "python_version >= '3.6'",
"version": "==1.23.23"
"version": "==1.23.24"
},
"cachetools": {
"hashes": [
@ -196,7 +196,7 @@
"sha256:1ef33f089e0a494e8d1b487508356f055c865b1955b125c00c991a4358543c80",
"sha256:8eca49962b1bfc09c24d442aa55688be88efe5c24aeef89d3be135614b95c678"
],
"markers": "python_version >= '3.7' and python_version < '4'",
"markers": "python_version >= '3.7' and python_full_version < '4.0.0'",
"version": "==1.9.0"
},
"cbor2": {
@ -325,7 +325,7 @@
"sha256:a0713dc7a1de3f06bc0df5a9567ad19ead2d3d5689b434768a6145bff77c0667",
"sha256:f184f0d851d96b6d29297354ed981b7dd71df7ff500d82fa6d11f0856bee8035"
],
"markers": "python_version < '4' and python_full_version >= '3.6.2'",
"markers": "python_full_version >= '3.6.2' and python_full_version < '4.0.0'",
"version": "==0.3.0"
},
"click-plugins": {
@ -494,11 +494,11 @@
},
"djangorestframework": {
"hashes": [
"sha256:6d1d59f623a5ad0509fe0d6bfe93cbdfe17b8116ebc8eda86d45f6e16e819aaf",
"sha256:f747949a8ddac876e879190df194b925c177cdeb725a099db1460872f7c0a7f2"
"sha256:48e64f08244fa0df9e2b8fbd405edec263d8e1251112a06d0073b546b7c86b9c",
"sha256:8b987d5683f5b3553dd946d4972048d3117fc526cb0bc01a3f021e81af53f39e"
],
"index": "pypi",
"version": "==3.12.4"
"version": "==3.13.0"
},
"djangorestframework-guardian": {
"hashes": [
@ -816,69 +816,69 @@
},
"lxml": {
"hashes": [
"sha256:11ae552a78612620afd15625be9f1b82e3cc2e634f90d6b11709b10a100cba59",
"sha256:121fc6f71c692b49af6c963b84ab7084402624ffbe605287da362f8af0668ea3",
"sha256:124f09614f999551ac65e5b9875981ce4b66ac4b8e2ba9284572f741935df3d9",
"sha256:12ae2339d32a2b15010972e1e2467345b7bf962e155671239fba74c229564b7f",
"sha256:12d8d6fe3ddef629ac1349fa89a638b296a34b6529573f5055d1cb4e5245f73b",
"sha256:1a2a7659b8eb93c6daee350a0d844994d49245a0f6c05c747f619386fb90ba04",
"sha256:1ccbfe5d17835db906f2bab6f15b34194db1a5b07929cba3cf45a96dbfbfefc0",
"sha256:2f77556266a8fe5428b8759fbfc4bd70be1d1d9c9b25d2a414f6a0c0b0f09120",
"sha256:3534d7c468c044f6aef3c0aff541db2826986a29ea73f2ca831f5d5284d9b570",
"sha256:3884476a90d415be79adfa4e0e393048630d0d5bcd5757c4c07d8b4b00a1096b",
"sha256:3b95fb7e6f9c2f53db88f4642231fc2b8907d854e614710996a96f1f32018d5c",
"sha256:46515773570a33eae13e451c8fcf440222ef24bd3b26f40774dd0bd8b6db15b2",
"sha256:46f21f2600d001af10e847df9eb3b832e8a439f696c04891bcb8a8cedd859af9",
"sha256:473701599665d874919d05bb33b56180447b3a9da8d52d6d9799f381ce23f95c",
"sha256:4b9390bf973e3907d967b75be199cf1978ca8443183cf1e78ad80ad8be9cf242",
"sha256:4f415624cf8b065796649a5e4621773dc5c9ea574a944c76a7f8a6d3d2906b41",
"sha256:534032a5ceb34bba1da193b7d386ac575127cc39338379f39a164b10d97ade89",
"sha256:558485218ee06458643b929765ac1eb04519ca3d1e2dcc288517de864c747c33",
"sha256:57cf05466917e08f90e323f025b96f493f92c0344694f5702579ab4b7e2eb10d",
"sha256:59d77bfa3bea13caee95bc0d3f1c518b15049b97dd61ea8b3d71ce677a67f808",
"sha256:5d5254c815c186744c8f922e2ce861a2bdeabc06520b4b30b2f7d9767791ce6e",
"sha256:5ea121cb66d7e5cb396b4c3ca90471252b94e01809805cfe3e4e44be2db3a99c",
"sha256:60aeb14ff9022d2687ef98ce55f6342944c40d00916452bb90899a191802137a",
"sha256:642eb4cabd997c9b949a994f9643cd8ae00cf4ca8c5cd9c273962296fadf1c44",
"sha256:6548fc551de15f310dd0564751d9dc3d405278d45ea9b2b369ed1eccf142e1f5",
"sha256:68a851176c931e2b3de6214347b767451243eeed3bea34c172127bbb5bf6c210",
"sha256:6e84edecc3a82f90d44ddee2ee2a2630d4994b8471816e226d2b771cda7ac4ca",
"sha256:73e8614258404b2689a26cb5d002512b8bc4dfa18aca86382f68f959aee9b0c8",
"sha256:7679bb6e4d9a3978a46ab19a3560e8d2b7265ef3c88152e7fdc130d649789887",
"sha256:76b6c296e4f7a1a8a128aec42d128646897f9ae9a700ef6839cdc9b3900db9b5",
"sha256:7f00cc64b49d2ef19ddae898a3def9dd8fda9c3d27c8a174c2889ee757918e71",
"sha256:8021eeff7fabde21b9858ed058a8250ad230cede91764d598c2466b0ba70db8b",
"sha256:87f8f7df70b90fbe7b49969f07b347e3f978f8bd1046bb8ecae659921869202b",
"sha256:916d457ad84e05b7db52700bad0a15c56e0c3000dcaf1263b2fb7a56fe148996",
"sha256:925174cafb0f1179a7fd38da90302555d7445e34c9ece68019e53c946be7f542",
"sha256:9801bcd52ac9c795a7d81ea67471a42cffe532e46cfb750cd5713befc5c019c0",
"sha256:99cf827f5a783038eb313beee6533dddb8bdb086d7269c5c144c1c952d142ace",
"sha256:a21b78af7e2e13bec6bea12fc33bc05730197674f3e5402ce214d07026ccfebd",
"sha256:a52e8f317336a44836475e9c802f51c2dc38d612eaa76532cb1d17690338b63b",
"sha256:a702005e447d712375433ed0499cb6e1503fadd6c96a47f51d707b4d37b76d3c",
"sha256:a708c291900c40a7ecf23f1d2384ed0bc0604e24094dd13417c7e7f8f7a50d93",
"sha256:a7790a273225b0c46e5f859c1327f0f659896cc72eaa537d23aa3ad9ff2a1cc1",
"sha256:abcf7daa5ebcc89328326254f6dd6d566adb483d4d00178892afd386ab389de2",
"sha256:add017c5bd6b9ec3a5f09248396b6ee2ce61c5621f087eb2269c813cd8813808",
"sha256:af4139172ff0263d269abdcc641e944c9de4b5d660894a3ec7e9f9db63b56ac9",
"sha256:b4015baed99d046c760f09a4c59d234d8f398a454380c3cf0b859aba97136090",
"sha256:ba0006799f21d83c3717fe20e2707a10bbc296475155aadf4f5850f6659b96b9",
"sha256:bdb98f4c9e8a1735efddfaa995b0c96559792da15d56b76428bdfc29f77c4cdb",
"sha256:c34234a1bc9e466c104372af74d11a9f98338a3f72fae22b80485171a64e0144",
"sha256:c580c2a61d8297a6e47f4d01f066517dbb019be98032880d19ece7f337a9401d",
"sha256:ca9a40497f7e97a2a961c04fa8a6f23d790b0521350a8b455759d786b0bcb203",
"sha256:cab343b265e38d4e00649cbbad9278b734c5715f9bcbb72c85a1f99b1a58e19a",
"sha256:ce52aad32ec6e46d1a91ff8b8014a91538800dd533914bfc4a82f5018d971408",
"sha256:da07c7e7fc9a3f40446b78c54dbba8bfd5c9100dfecb21b65bfe3f57844f5e71",
"sha256:dc8a0dbb2a10ae8bb609584f5c504789f0f3d0d81840da4849102ec84289f952",
"sha256:e5b4b0d9440046ead3bd425eb2b852499241ee0cef1ae151038e4f87ede888c4",
"sha256:f33d8efb42e4fc2b31b3b4527940b25cdebb3026fb56a80c1c1c11a4271d2352",
"sha256:f6befb83bca720b71d6bd6326a3b26e9496ae6649e26585de024890fe50f49b8",
"sha256:fcc849b28f584ed1dbf277291ded5c32bb3476a37032df4a1d523b55faa5f944",
"sha256:ff44de36772b05c2eb74f2b4b6d1ae29b8f41ed5506310ce1258d44826ee38c1"
"sha256:0607ff0988ad7e173e5ddf7bf55ee65534bd18a5461183c33e8e41a59e89edf4",
"sha256:09b738360af8cb2da275998a8bf79517a71225b0de41ab47339c2beebfff025f",
"sha256:0a5f0e4747f31cff87d1eb32a6000bde1e603107f632ef4666be0dc065889c7a",
"sha256:0b5e96e25e70917b28a5391c2ed3ffc6156513d3db0e1476c5253fcd50f7a944",
"sha256:1104a8d47967a414a436007c52f533e933e5d52574cab407b1e49a4e9b5ddbd1",
"sha256:13dbb5c7e8f3b6a2cf6e10b0948cacb2f4c9eb05029fe31c60592d08ac63180d",
"sha256:2a906c3890da6a63224d551c2967413b8790a6357a80bf6b257c9a7978c2c42d",
"sha256:317bd63870b4d875af3c1be1b19202de34c32623609ec803b81c99193a788c1e",
"sha256:34c22eb8c819d59cec4444d9eebe2e38b95d3dcdafe08965853f8799fd71161d",
"sha256:36b16fecb10246e599f178dd74f313cbdc9f41c56e77d52100d1361eed24f51a",
"sha256:38d9759733aa04fb1697d717bfabbedb21398046bd07734be7cccc3d19ea8675",
"sha256:3e26ad9bc48d610bf6cc76c506b9e5ad9360ed7a945d9be3b5b2c8535a0145e3",
"sha256:41358bfd24425c1673f184d7c26c6ae91943fe51dfecc3603b5e08187b4bcc55",
"sha256:447d5009d6b5447b2f237395d0018901dcc673f7d9f82ba26c1b9f9c3b444b60",
"sha256:44f552e0da3c8ee3c28e2eb82b0b784200631687fc6a71277ea8ab0828780e7d",
"sha256:490712b91c65988012e866c411a40cc65b595929ececf75eeb4c79fcc3bc80a6",
"sha256:4c093c571bc3da9ebcd484e001ba18b8452903cd428c0bc926d9b0141bcb710e",
"sha256:50d3dba341f1e583265c1a808e897b4159208d814ab07530202b6036a4d86da5",
"sha256:534e946bce61fd162af02bad7bfd2daec1521b71d27238869c23a672146c34a5",
"sha256:585ea241ee4961dc18a95e2f5581dbc26285fcf330e007459688096f76be8c42",
"sha256:59e7da839a1238807226f7143c68a479dee09244d1b3cf8c134f2fce777d12d0",
"sha256:5b0f782f0e03555c55e37d93d7a57454efe7495dab33ba0ccd2dbe25fc50f05d",
"sha256:5bee1b0cbfdb87686a7fb0e46f1d8bd34d52d6932c0723a86de1cc532b1aa489",
"sha256:610807cea990fd545b1559466971649e69302c8a9472cefe1d6d48a1dee97440",
"sha256:6308062534323f0d3edb4e702a0e26a76ca9e0e23ff99be5d82750772df32a9e",
"sha256:67fa5f028e8a01e1d7944a9fb616d1d0510d5d38b0c41708310bd1bc45ae89f6",
"sha256:6a2ab9d089324d77bb81745b01f4aeffe4094306d939e92ba5e71e9a6b99b71e",
"sha256:6c198bfc169419c09b85ab10cb0f572744e686f40d1e7f4ed09061284fc1303f",
"sha256:6e56521538f19c4a6690f439fefed551f0b296bd785adc67c1777c348beb943d",
"sha256:6ec829058785d028f467be70cd195cd0aaf1a763e4d09822584ede8c9eaa4b03",
"sha256:718d7208b9c2d86aaf0294d9381a6acb0158b5ff0f3515902751404e318e02c9",
"sha256:735e3b4ce9c0616e85f302f109bdc6e425ba1670a73f962c9f6b98a6d51b77c9",
"sha256:772057fba283c095db8c8ecde4634717a35c47061d24f889468dc67190327bcd",
"sha256:7b5e2acefd33c259c4a2e157119c4373c8773cf6793e225006a1649672ab47a6",
"sha256:82d16a64236970cb93c8d63ad18c5b9f138a704331e4b916b2737ddfad14e0c4",
"sha256:87c1b0496e8c87ec9db5383e30042357b4839b46c2d556abd49ec770ce2ad868",
"sha256:8e54945dd2eeb50925500957c7c579df3cd07c29db7810b83cf30495d79af267",
"sha256:9393a05b126a7e187f3e38758255e0edf948a65b22c377414002d488221fdaa2",
"sha256:9fbc0dee7ff5f15c4428775e6fa3ed20003140560ffa22b88326669d53b3c0f4",
"sha256:a1613838aa6b89af4ba10a0f3a972836128801ed008078f8c1244e65958f1b24",
"sha256:a1bbc4efa99ed1310b5009ce7f3a1784698082ed2c1ef3895332f5df9b3b92c2",
"sha256:a555e06566c6dc167fbcd0ad507ff05fd9328502aefc963cb0a0547cfe7f00db",
"sha256:a58d78653ae422df6837dd4ca0036610b8cb4962b5cfdbd337b7b24de9e5f98a",
"sha256:a5edc58d631170de90e50adc2cc0248083541affef82f8cd93bea458e4d96db8",
"sha256:a5f623aeaa24f71fce3177d7fee875371345eb9102b355b882243e33e04b7175",
"sha256:adaab25be351fff0d8a691c4f09153647804d09a87a4e4ea2c3f9fe9e8651851",
"sha256:ade74f5e3a0fd17df5782896ddca7ddb998845a5f7cd4b0be771e1ffc3b9aa5b",
"sha256:b1d381f58fcc3e63fcc0ea4f0a38335163883267f77e4c6e22d7a30877218a0e",
"sha256:bf6005708fc2e2c89a083f258b97709559a95f9a7a03e59f805dd23c93bc3986",
"sha256:d546431636edb1d6a608b348dd58cc9841b81f4116745857b6cb9f8dadb2725f",
"sha256:d5618d49de6ba63fe4510bdada62d06a8acfca0b4b5c904956c777d28382b419",
"sha256:dfd0d464f3d86a1460683cd742306d1138b4e99b79094f4e07e1ca85ee267fe7",
"sha256:e18281a7d80d76b66a9f9e68a98cf7e1d153182772400d9a9ce855264d7d0ce7",
"sha256:e410cf3a2272d0a85526d700782a2fa92c1e304fdcc519ba74ac80b8297adf36",
"sha256:e662c6266e3a275bdcb6bb049edc7cd77d0b0f7e119a53101d367c841afc66dc",
"sha256:ec9027d0beb785a35aa9951d14e06d48cfbf876d8ff67519403a2522b181943b",
"sha256:eed394099a7792834f0cb4a8f615319152b9d801444c1c9e1b1a2c36d2239f9e",
"sha256:f76dbe44e31abf516114f6347a46fa4e7c2e8bceaa4b6f7ee3a0a03c8eba3c17",
"sha256:fc15874816b9320581133ddc2096b644582ab870cf6a6ed63684433e7af4b0d3",
"sha256:fc9fb11b65e7bc49f7f75aaba1b700f7181d95d4e151cf2f24d51bfd14410b77"
],
"index": "pypi",
"version": "==4.6.5"
"version": "==4.7.1"
},
"maxminddb": {
"hashes": [
@ -1312,12 +1312,12 @@
"version": "==0.5.0"
},
"sentry-sdk": {
"git": "https://github.com/beryju/sentry-python.git",
"hashes": [
"sha256:0db297ab32e095705c20f742c3a5dac62fe15c4318681884053d0898e5abb2f6",
"sha256:789a11a87ca02491896e121efdd64e8fd93327b69e8f2f7d42f03e2569648e88"
],
"index": "pypi",
"version": "==1.5.0"
"ref": "379aee28b15d3b87b381317746c4efd24b3d7bc3"
},
"service-identity": {
"hashes": [
@ -2387,11 +2387,11 @@
},
"tomli": {
"hashes": [
"sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee",
"sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"
"sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f",
"sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"
],
"markers": "python_version >= '3.6'",
"version": "==1.2.2"
"version": "==1.2.3"
},
"trio": {
"hashes": [

View file

@ -1,13 +1,6 @@
"""authentik administration metrics"""
import time
from collections import Counter
from datetime import timedelta
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.utils.timezone import now
from drf_spectacular.utils import extend_schema, extend_schema_field
from guardian.shortcuts import get_objects_for_user
from rest_framework.fields import IntegerField, SerializerMethodField
from rest_framework.permissions import IsAdminUser
from rest_framework.request import Request
@ -15,31 +8,7 @@ from rest_framework.response import Response
from rest_framework.views import APIView
from authentik.core.api.utils import PassiveSerializer
from authentik.events.models import Event, EventAction
def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
Event.objects.filter(created__gte=date_from, **filter_kwargs)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
from authentik.events.models import EventAction
class CoordinateSerializer(PassiveSerializer):
@ -58,12 +27,22 @@ class LoginMetricsSerializer(PassiveSerializer):
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN)
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
return get_events_per_1h(action=EventAction.LOGIN_FAILED)
user = self.context["user"]
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED)
.get_events_per_hour()
)
class AdministrationMetricsViewSet(APIView):
@ -75,4 +54,5 @@ class AdministrationMetricsViewSet(APIView):
def get(self, request: Request) -> Response:
"""Login Metrics per 1h"""
serializer = LoginMetricsSerializer(True)
serializer.context["user"] = request.user
return Response(serializer.data)

View file

@ -5,6 +5,7 @@ from django.http.response import HttpResponseBadRequest
from django.shortcuts import get_object_or_404
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, OpenApiResponse, extend_schema
from guardian.shortcuts import get_objects_for_user
from rest_framework.decorators import action
from rest_framework.fields import ReadOnlyField
from rest_framework.parsers import MultiPartParser
@ -15,7 +16,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.providers import ProviderSerializer
from authentik.core.api.used_by import UsedByMixin
@ -239,8 +240,10 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
"""Metrics for application logins"""
app = self.get_object()
return Response(
get_events_per_1h(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(
action=EventAction.AUTHORIZE_APPLICATION,
context__authorized_application__pk=app.pk.hex,
)
.get_events_per_hour()
)

View file

@ -104,14 +104,14 @@ class SourceViewSet(
)
matching_sources: list[UserSettingSerializer] = []
for source in _all_sources:
user_settings = source.ui_user_settings
user_settings = source.ui_user_settings()
if not user_settings:
continue
policy_engine = PolicyEngine(source, request.user, request)
policy_engine.build()
if not policy_engine.passing:
continue
source_settings = source.ui_user_settings
source_settings = source.ui_user_settings()
source_settings.initial_data["object_uid"] = source.slug
if not source_settings.is_valid():
LOGGER.warning(source_settings.errors)

View file

@ -38,7 +38,7 @@ from rest_framework.viewsets import ModelViewSet
from rest_framework_guardian.filters import ObjectPermissionsFilter
from structlog.stdlib import get_logger
from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.api.decorators import permission_required
from authentik.core.api.groups import GroupSerializer
from authentik.core.api.used_by import UsedByMixin
@ -184,19 +184,31 @@ class UserMetricsSerializer(PassiveSerializer):
def get_logins_per_1h(self, _):
"""Get successful logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.LOGIN, user__pk=user.pk)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN, user__pk=user.pk)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_logins_failed_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.LOGIN_FAILED, context__username=user.username)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.LOGIN_FAILED, context__username=user.username)
.get_events_per_hour()
)
@extend_schema_field(CoordinateSerializer(many=True))
def get_authorizations_per_1h(self, _):
"""Get failed logins per hour for the last 24 hours"""
user = self.context["user"]
return get_events_per_1h(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
return (
get_objects_for_user(user, "authentik_events.view_event")
.filter(action=EventAction.AUTHORIZE_APPLICATION, user__pk=user.pk)
.get_events_per_hour()
)
class UsersFilter(FilterSet):

View file

@ -5,6 +5,7 @@ from typing import Callable
from uuid import uuid4
from django.http import HttpRequest, HttpResponse
from sentry_sdk.api import set_tag
SESSION_IMPERSONATE_USER = "authentik_impersonate_user"
SESSION_IMPERSONATE_ORIGINAL_USER = "authentik_impersonate_original_user"
@ -50,6 +51,7 @@ class RequestIDMiddleware:
"request_id": request_id,
"host": request.get_host(),
}
set_tag("authentik.request_id", request_id)
response = self.get_response(request)
response[RESPONSE_HEADER_ID] = request.request_id
setattr(response, "ak_context", {})

View file

@ -359,13 +359,11 @@ class Source(ManagedModel, SerializerModel, PolicyBindingModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def ui_login_button(self) -> Optional[UILoginButton]:
def ui_login_button(self, request: HttpRequest) -> Optional[UILoginButton]:
"""If source uses a http-based flow, return UI Information about the login
button. If source doesn't use http-based flow, return None."""
return None
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or UserSettingSerializer."""

View file

@ -19,6 +19,7 @@
<script src="{% static 'dist/poly.js' %}" type="module"></script>
{% block head %}
{% endblock %}
<meta name="sentry-trace" content="{{ sentry_trace }}" />
</head>
<body>
{% block body %}

View file

@ -2,7 +2,7 @@
from time import sleep
from typing import Callable, Type
from django.test import TestCase
from django.test import RequestFactory, TestCase
from django.utils.timezone import now
from guardian.shortcuts import get_anonymous_user
@ -30,6 +30,9 @@ class TestModels(TestCase):
def source_tester_factory(test_model: Type[Stage]) -> Callable:
"""Test source"""
factory = RequestFactory()
request = factory.get("/")
def tester(self: TestModels):
model_class = None
if test_model._meta.abstract:
@ -38,8 +41,8 @@ def source_tester_factory(test_model: Type[Stage]) -> Callable:
model_class = test_model()
model_class.slug = "test"
self.assertIsNotNone(model_class.component)
_ = model_class.ui_login_button
_ = model_class.ui_user_settings
_ = model_class.ui_login_button(request)
_ = model_class.ui_user_settings()
return tester

View file

@ -41,7 +41,7 @@ class TestPropertyMappingAPI(APITestCase):
expr = "return True"
self.assertEqual(PropertyMappingSerializer().validate_expression(expr), expr)
with self.assertRaises(ValidationError):
print(PropertyMappingSerializer().validate_expression("/"))
PropertyMappingSerializer().validate_expression("/")
def test_types(self):
"""Test PropertyMappigns's types endpoint"""

View file

@ -11,10 +11,13 @@ from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509 import Certificate, load_pem_x509_certificate
from django.db import models
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
from authentik.lib.models import CreatedUpdatedModel
from authentik.managed.models import ManagedModel
LOGGER = get_logger()
class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
"""CertificateKeyPair that can be used for signing or encrypting if `key_data`
@ -62,7 +65,8 @@ class CertificateKeyPair(ManagedModel, CreatedUpdatedModel):
password=None,
backend=default_backend(),
)
except ValueError:
except ValueError as exc:
LOGGER.warning(exc)
return None
return self._private_key

View file

@ -2,6 +2,9 @@
from glob import glob
from pathlib import Path
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.x509.base import load_pem_x509_certificate
from django.utils.translation import gettext_lazy as _
from structlog.stdlib import get_logger
@ -20,6 +23,22 @@ LOGGER = get_logger()
MANAGED_DISCOVERED = "goauthentik.io/crypto/discovered/%s"
def ensure_private_key_valid(body: str):
"""Attempt loading of an RSA Private key without password"""
load_pem_private_key(
str.encode("\n".join([x.strip() for x in body.split("\n")])),
password=None,
backend=default_backend(),
)
return body
def ensure_certificate_valid(body: str):
"""Attempt loading of a PEM-encoded certificate"""
load_pem_x509_certificate(body.encode("utf-8"), default_backend())
return body
@CELERY_APP.task(bind=True, base=MonitoredTask)
@prefill_task
def certificate_discovery(self: MonitoredTask):
@ -42,11 +61,11 @@ def certificate_discovery(self: MonitoredTask):
with open(path, "r+", encoding="utf-8") as _file:
body = _file.read()
if "BEGIN RSA PRIVATE KEY" in body:
private_keys[cert_name] = body
private_keys[cert_name] = ensure_private_key_valid(body)
else:
certs[cert_name] = body
except OSError as exc:
LOGGER.warning("Failed to open file", exc=exc, file=path)
certs[cert_name] = ensure_certificate_valid(body)
except (OSError, ValueError) as exc:
LOGGER.warning("Failed to open file or invalid format", exc=exc, file=path)
discovered += 1
for name, cert_data in certs.items():
cert = CertificateKeyPair.objects.filter(managed=MANAGED_DISCOVERED % name).first()

View file

@ -1,4 +1,6 @@
"""Events API Views"""
from json import loads
import django_filters
from django.db.models.aggregates import Count
from django.db.models.fields.json import KeyTextTransform
@ -12,6 +14,7 @@ from rest_framework.response import Response
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.admin.api.metrics import CoordinateSerializer
from authentik.core.api.utils import PassiveSerializer, TypeCreateSerializer
from authentik.events.models import Event, EventAction
@ -110,13 +113,20 @@ class EventViewSet(ModelViewSet):
@extend_schema(
methods=["GET"],
responses={200: EventTopPerUserSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"top_n",
type=OpenApiTypes.INT,
location=OpenApiParameter.QUERY,
required=False,
)
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
@ -137,6 +147,40 @@ class EventViewSet(ModelViewSet):
.order_by("-counted_events")[:top_n]
)
@extend_schema(
methods=["GET"],
responses={200: CoordinateSerializer(many=True)},
filters=[],
parameters=[
OpenApiParameter(
"action",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
OpenApiParameter(
"query",
type=OpenApiTypes.STR,
location=OpenApiParameter.QUERY,
required=False,
),
],
)
@action(detail=False, methods=["GET"], pagination_class=None)
def per_month(self, request: Request):
"""Get the count of events per month"""
filtered_action = request.query_params.get("action", EventAction.LOGIN)
try:
query = loads(request.query_params.get("query", "{}"))
except ValueError:
return Response(status=400)
return Response(
get_objects_for_user(request.user, "authentik_events.view_event")
.filter(action=filtered_action)
.filter(**query)
.get_events_per_day()
)
@extend_schema(responses={200: TypeCreateSerializer(many=True)})
@action(detail=False, pagination_class=None, filter_backends=[])
def actions(self, request: Request) -> Response:

View file

@ -7,6 +7,7 @@ from typing import Optional, TypedDict
from geoip2.database import Reader
from geoip2.errors import GeoIP2Error
from geoip2.models import City
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.lib.config import CONFIG
@ -62,13 +63,17 @@ class GeoIPReader:
def city(self, ip_address: str) -> Optional[City]:
"""Wrapper for Reader.city"""
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
with Hub.current.start_span(
op="authentik.events.geo.city",
description=ip_address,
):
if not self.enabled:
return None
self.__check_expired()
try:
return self.__reader.city(ip_address)
except (GeoIP2Error, ValueError):
return None
def city_dict(self, ip_address: str) -> Optional[GeoIPDict]:
"""Wrapper for self.city that returns a dict"""

View file

@ -314,169 +314,10 @@ class Migration(migrations.Migration):
old_name="user_json",
new_name="user",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("sign_up", "Sign Up"),
("authorize_application", "Authorize Application"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RemoveField(
model_name="event",
name="date",
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_created", "Invite Created"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.CreateModel(
name="NotificationTransport",
fields=[
@ -610,68 +451,6 @@ class Migration(migrations.Migration):
help_text="Only send notification once, for example when sending a webhook into a chat channel.",
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("token_view", "Token View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.RunPython(
code=token_view_to_secret_view,
),
@ -688,76 +467,11 @@ class Migration(migrations.Migration):
migrations.RunPython(
code=update_expires,
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AddField(
model_name="event",
name="tenant",
field=models.JSONField(blank=True, default=authentik.events.models.default_tenant),
),
migrations.AlterField(
model_name="event",
name="action",
field=models.TextField(
choices=[
("login", "Login"),
("login_failed", "Login Failed"),
("logout", "Logout"),
("user_write", "User Write"),
("suspicious_request", "Suspicious Request"),
("password_set", "Password Set"),
("secret_view", "Secret View"),
("invitation_used", "Invite Used"),
("authorize_application", "Authorize Application"),
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),
("system_task_execution", "System Task Execution"),
("system_task_exception", "System Task Exception"),
("system_exception", "System Exception"),
("configuration_error", "Configuration Error"),
("model_created", "Model Created"),
("model_updated", "Model Updated"),
("model_deleted", "Model Deleted"),
("email_sent", "Email Sent"),
("update_available", "Update Available"),
("custom_", "Custom Prefix"),
]
),
),
migrations.AlterField(
model_name="event",
name="action",
@ -776,6 +490,7 @@ class Migration(migrations.Migration):
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("flow_execution", "Flow Execution"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),

View file

@ -1,4 +1,6 @@
"""authentik events models"""
import time
from collections import Counter
from datetime import timedelta
from inspect import getmodule, stack
from smtplib import SMTPException
@ -7,6 +9,12 @@ from uuid import uuid4
from django.conf import settings
from django.db import models
from django.db.models import Count, ExpressionWrapper, F
from django.db.models.fields import DurationField
from django.db.models.functions import ExtractHour
from django.db.models.functions.datetime import ExtractDay
from django.db.models.manager import Manager
from django.db.models.query import QuerySet
from django.http import HttpRequest
from django.http.request import QueryDict
from django.utils.timezone import now
@ -70,6 +78,7 @@ class EventAction(models.TextChoices):
IMPERSONATION_STARTED = "impersonation_started"
IMPERSONATION_ENDED = "impersonation_ended"
FLOW_EXECUTION = "flow_execution"
POLICY_EXECUTION = "policy_execution"
POLICY_EXCEPTION = "policy_exception"
PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception"
@ -90,6 +99,72 @@ class EventAction(models.TextChoices):
CUSTOM_PREFIX = "custom_"
class EventQuerySet(QuerySet):
"""Custom events query set with helper functions"""
def get_events_per_hour(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(days=1)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_hours=ExtractHour("age"))
.values("age_hours")
.annotate(count=Count("pk"))
.order_by("age_hours")
)
data = Counter({int(d["age_hours"]): d["count"] for d in result})
results = []
_now = now()
for hour in range(0, -24, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000,
"y_cord": data[hour * -1],
}
)
return results
def get_events_per_day(self) -> list[dict[str, int]]:
"""Get event count by hour in the last day, fill with zeros"""
date_from = now() - timedelta(weeks=4)
result = (
self.filter(created__gte=date_from)
.annotate(age=ExpressionWrapper(now() - F("created"), output_field=DurationField()))
.annotate(age_days=ExtractDay("age"))
.values("age_days")
.annotate(count=Count("pk"))
.order_by("age_days")
)
data = Counter({int(d["age_days"]): d["count"] for d in result})
results = []
_now = now()
for day in range(0, -30, -1):
results.append(
{
"x_cord": time.mktime((_now + timedelta(days=day)).timetuple()) * 1000,
"y_cord": data[day * -1],
}
)
return results
class EventManager(Manager):
"""Custom helper methods for Events"""
def get_queryset(self) -> QuerySet:
"""use custom queryset"""
return EventQuerySet(self.model, using=self._db)
def get_events_per_hour(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_hour()
def get_events_per_day(self) -> list[dict[str, int]]:
"""Wrap method from queryset"""
return self.get_queryset().get_events_per_day()
class Event(ExpiringModel):
"""An individual Audit/Metrics/Notification/Error Event"""
@ -105,6 +180,8 @@ class Event(ExpiringModel):
# Shadow the expires attribute from ExpiringModel to override the default duration
expires = models.DateTimeField(default=default_event_duration)
objects = EventManager()
@staticmethod
def _get_app_from_request(request: HttpRequest) -> str:
if not isinstance(request, HttpRequest):

View file

@ -46,7 +46,7 @@ class TaskResult:
def with_error(self, exc: Exception) -> "TaskResult":
"""Since errors might not always be pickle-able, set the traceback"""
self.messages.extend(exception_to_string(exc).splitlines())
self.messages.append(str(exc))
return self

View file

@ -90,7 +90,7 @@ class StageViewSet(
stages += list(configurable_stage.objects.all().order_by("name"))
matching_stages: list[dict] = []
for stage in stages:
user_settings = stage.ui_user_settings
user_settings = stage.ui_user_settings()
if not user_settings:
continue
user_settings.initial_data["object_uid"] = str(stage.pk)

View file

@ -75,7 +75,6 @@ class Stage(SerializerModel):
"""Return component used to edit this object"""
raise NotImplementedError
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
"""Entrypoint to integrate with User settings. Can either return None if no
user settings are available, or a challenge."""

View file

@ -126,7 +126,9 @@ class FlowPlanner:
) -> FlowPlan:
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
and return ordered list"""
with Hub.current.start_span(op="flow.planner.plan", description=self.flow.slug) as span:
with Hub.current.start_span(
op="authentik.flow.planner.plan", description=self.flow.slug
) as span:
span: Span
span.set_data("flow", self.flow)
span.set_data("request", request)
@ -181,7 +183,7 @@ class FlowPlanner:
"""Build flow plan by checking each stage in their respective
order and checking the applied policies"""
with Hub.current.start_span(
op="flow.planner.build_plan",
op="authentik.flow.planner.build_plan",
description=self.flow.slug,
) as span, HIST_FLOWS_PLAN_TIME.labels(flow_slug=self.flow.slug).time():
span: Span

View file

@ -6,6 +6,7 @@ from django.http.response import HttpResponse
from django.urls import reverse
from django.views.generic.base import View
from rest_framework.request import Request
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.models import DEFAULT_AVATAR, User
@ -94,8 +95,16 @@ class ChallengeStageView(StageView):
keep_context=keep_context,
)
return self.executor.restart_flow(keep_context)
return self.challenge_invalid(challenge)
return self.challenge_valid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_invalid",
description=self.__class__.__name__,
):
return self.challenge_invalid(challenge)
with Hub.current.start_span(
op="authentik.flow.stage.challenge_valid",
description=self.__class__.__name__,
):
return self.challenge_valid(challenge)
def format_title(self) -> str:
"""Allow usage of placeholder in flow title."""
@ -104,7 +113,11 @@ class ChallengeStageView(StageView):
}
def _get_challenge(self, *args, **kwargs) -> Challenge:
challenge = self.get_challenge(*args, **kwargs)
with Hub.current.start_span(
op="authentik.flow.stage.get_challenge",
description=self.__class__.__name__,
):
challenge = self.get_challenge(*args, **kwargs)
if "flow_info" not in challenge.initial_data:
flow_info = ContextualFlowInfo(
data={

View file

@ -32,7 +32,7 @@ class TestFlowsAPI(APITestCase):
def test_models(self):
"""Test that ui_user_settings returns none"""
self.assertIsNone(Stage().ui_user_settings)
self.assertIsNone(Stage().ui_user_settings())
def test_api_serializer(self):
"""Test that stage serializer returns the correct type"""

View file

@ -23,7 +23,7 @@ def model_tester_factory(test_model: Type[Stage]) -> Callable:
model_class = test_model()
self.assertTrue(issubclass(model_class.type, StageView))
self.assertIsNotNone(test_model.component)
_ = model_class.ui_user_settings
_ = model_class.ui_user_settings()
return tester

View file

@ -160,7 +160,7 @@ class FlowExecutorView(APIView):
# pylint: disable=unused-argument, too-many-return-statements
def dispatch(self, request: HttpRequest, flow_slug: str) -> HttpResponse:
with Hub.current.start_span(
op="flow.executor.dispatch", description=self.flow.slug
op="authentik.flow.executor.dispatch", description=self.flow.slug
) as span:
span.set_data("authentik Flow", self.flow.slug)
get_params = QueryDict(request.GET.get("query", ""))
@ -275,7 +275,7 @@ class FlowExecutorView(APIView):
)
try:
with Hub.current.start_span(
op="flow.executor.stage",
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "GET")
@ -319,7 +319,7 @@ class FlowExecutorView(APIView):
)
try:
with Hub.current.start_span(
op="flow.executor.stage",
op="authentik.flow.executor.stage",
description=class_to_path(self.current_stage_view.__class__),
) as span:
span.set_data("Method", "POST")
@ -371,6 +371,12 @@ class FlowExecutorView(APIView):
NEXT_ARG_NAME, "authentik_core:root-redirect"
)
self.cancel()
Event.new(
action=EventAction.FLOW_EXECUTION,
flow=self.flow,
designation=self.flow.designation,
successful=True,
).from_http(self.request)
return to_stage_response(self.request, redirect_with_qs(next_param))
def stage_ok(self) -> HttpResponse:

View file

@ -106,7 +106,7 @@ class FlowInspectorView(APIView):
else:
try:
current_plan = request.session[SESSION_KEY_HISTORY][-1]
except KeyError:
except IndexError:
return Response(status=400)
is_completed = True
current_serializer = FlowInspectorPlanSerializer(

View file

@ -80,8 +80,9 @@ class BaseEvaluator:
"""Parse and evaluate expression. If the syntax is incorrect, a SyntaxError is raised.
If any exception is raised during execution, it is raised.
The result is returned without any type-checking."""
with Hub.current.start_span(op="lib.evaluator.evaluate") as span:
with Hub.current.start_span(op="authentik.lib.evaluator.evaluate") as span:
span: Span
span.description = self._filename
span.set_data("expression", expression_source)
param_keys = self._context.keys()
try:

View file

@ -90,7 +90,7 @@ class PolicyEngine:
def build(self) -> "PolicyEngine":
"""Build wrapper which monitors performance"""
with Hub.current.start_span(
op="policy.engine.build",
op="authentik.policy.engine.build",
description=self.__pbm,
) as span, HIST_POLICIES_BUILD_TIME.labels(
object_name=self.__pbm,

View file

@ -66,6 +66,7 @@ class Migration(migrations.Migration):
("source_linked", "Source Linked"),
("impersonation_started", "Impersonation Started"),
("impersonation_ended", "Impersonation Ended"),
("flow_execution", "Flow Execution"),
("policy_execution", "Policy Execution"),
("policy_exception", "Policy Exception"),
("property_mapping_exception", "Property Mapping Exception"),

View file

@ -74,4 +74,4 @@ class TestExpressionPolicyAPI(APITestCase):
expr = "return True"
self.assertEqual(ExpressionPolicySerializer().validate_expression(expr), expr)
with self.assertRaises(ValidationError):
print(ExpressionPolicySerializer().validate_expression("/"))
ExpressionPolicySerializer().validate_expression("/")

View file

@ -130,7 +130,7 @@ class PolicyProcess(PROCESS_CLASS):
def profiling_wrapper(self):
"""Run with profiling enabled"""
with Hub.current.start_span(
op="policy.process.execute",
op="authentik.policy.process.execute",
) as span, HIST_POLICIES_EXECUTION_TIME.labels(
binding_order=self.binding.order,
binding_target_type=self.binding.target_type,

View file

@ -8,7 +8,6 @@ from datetime import datetime
from hashlib import sha256
from typing import Any, Optional, Type
from urllib.parse import urlparse
from uuid import uuid4
from dacite import from_dict
from django.db import models
@ -225,7 +224,7 @@ class OAuth2Provider(Provider):
token = RefreshToken(
user=user,
provider=self,
refresh_token=uuid4().hex,
refresh_token=generate_key(),
expires=timezone.now() + timedelta_from_string(self.token_validity),
scope=scope,
)
@ -434,7 +433,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
"""Create access token with a similar format as Okta, Keycloak, ADFS"""
token = self.create_id_token(user, request).to_dict()
token["cid"] = self.provider.client_id
token["uid"] = uuid4().hex
token["uid"] = generate_key()
return self.provider.encode(token)
def create_id_token(self, user: User, request: HttpRequest) -> IDToken:

View file

@ -194,8 +194,10 @@ class TokenView(View):
self.params = TokenParams.parse(request, self.provider, client_id, client_secret)
if self.params.grant_type == GRANT_TYPE_AUTHORIZATION_CODE:
LOGGER.info("Converting authorization code to refresh token")
return TokenResponse(self.create_code_response())
if self.params.grant_type == GRANT_TYPE_REFRESH_TOKEN:
LOGGER.info("Refreshing refresh token")
return TokenResponse(self.create_refresh_response())
raise ValueError(f"Invalid grant_type: {self.params.grant_type}")
except TokenError as error:

View file

@ -70,13 +70,14 @@ class AssertionProcessor:
"""Get AttributeStatement Element with Attributes from Property Mappings."""
# https://commons.lbl.gov/display/IDMgmt/Attribute+Definitions
attribute_statement = Element(f"{{{NS_SAML_ASSERTION}}}AttributeStatement")
user = self.http_request.user
for mapping in self.provider.property_mappings.all().select_subclasses():
if not isinstance(mapping, SAMLPropertyMapping):
continue
try:
mapping: SAMLPropertyMapping
value = mapping.evaluate(
user=self.http_request.user,
user=user,
request=self.http_request,
provider=self.provider,
)

View file

@ -1,7 +1,6 @@
"""Source API Views"""
from typing import Any
from django.utils.text import slugify
from django_filters.filters import AllValuesMultipleFilter
from django_filters.filterset import FilterSet
from drf_spectacular.types import OpenApiTypes
@ -110,7 +109,8 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
GroupLDAPSynchronizer,
MembershipLDAPSynchronizer,
]:
task = TaskInfo.by_name(f"ldap_sync_{slugify(source.name)}-{sync_class.__name__}")
sync_name = sync_class.__name__.replace("LDAPSynchronizer", "").lower()
task = TaskInfo.by_name(f"ldap_sync_{source.slug}_{sync_name}")
if task:
results.append(task)
return Response(TaskSerializer(results, many=True).data)

View file

@ -29,7 +29,7 @@ class GroupLDAPSynchronizer(BaseLDAPSynchronizer):
group_dn = self._flatten(self._flatten(group.get("entryDN", group.get("dn"))))
if self._source.object_uniqueness_field not in attributes:
self.message(
f"Cannot find uniqueness field in attributes: '{group_dn}",
f"Cannot find uniqueness field in attributes: '{group_dn}'",
attributes=attributes.keys(),
dn=group_dn,
)

View file

@ -31,7 +31,7 @@ class UserLDAPSynchronizer(BaseLDAPSynchronizer):
user_dn = self._flatten(user.get("entryDN", user.get("dn")))
if self._source.object_uniqueness_field not in attributes:
self.message(
f"Cannot find uniqueness field in attributes: '{user_dn}",
f"Cannot find uniqueness field in attributes: '{user_dn}'",
attributes=attributes.keys(),
dn=user_dn,
)

View file

@ -1,5 +1,4 @@
"""LDAP Sync tasks"""
from django.utils.text import slugify
from ldap3.core.exceptions import LDAPException
from structlog.stdlib import get_logger
@ -39,7 +38,7 @@ def ldap_sync(self: MonitoredTask, source_pk: str, sync_class: str):
# to set the state with
return
sync = path_to_class(sync_class)
self.set_uid(f"{slugify(source.name)}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
self.set_uid(f"{source.slug}_{sync.__name__.replace('LDAPSynchronizer', '').lower()}")
try:
sync_inst = sync(source)
count = sync_inst.sync()

View file

@ -14,6 +14,7 @@ AUTHENTIK_SOURCES_OAUTH_TYPES = [
"authentik.sources.oauth.types.github",
"authentik.sources.oauth.types.google",
"authentik.sources.oauth.types.oidc",
"authentik.sources.oauth.types.okta",
"authentik.sources.oauth.types.reddit",
"authentik.sources.oauth.types.twitter",
]

View file

@ -2,13 +2,13 @@
from typing import TYPE_CHECKING, Optional, Type
from django.db import models
from django.http.request import HttpRequest
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton, UserSettingSerializer
from authentik.flows.challenge import ChallengeTypes, RedirectChallenge
if TYPE_CHECKING:
from authentik.sources.oauth.types.manager import SourceType
@ -64,24 +64,15 @@ class OAuthSource(Source):
return OAuthSourceSerializer
@property
def ui_login_button(self) -> UILoginButton:
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
provider_type = self.type
provider = provider_type()
return UILoginButton(
challenge=RedirectChallenge(
instance={
"type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": self.slug},
),
}
),
icon_url=provider_type().icon_url(),
name=self.name,
icon_url=provider.icon_url(),
challenge=provider.login_challenge(self, request),
)
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={
@ -183,6 +174,16 @@ class AppleOAuthSource(OAuthSource):
verbose_name_plural = _("Apple OAuth Sources")
class OktaOAuthSource(OAuthSource):
"""Login using a okta.com."""
class Meta:
abstract = True
verbose_name = _("Okta OAuth Source")
verbose_name_plural = _("Okta OAuth Sources")
class UserOAuthSourceConnection(UserSourceConnection):
"""Authorized remote OAuth provider."""

View file

@ -2,10 +2,15 @@
from time import time
from typing import Any, Optional
from django.http.request import HttpRequest
from django.urls.base import reverse
from jwt import decode, encode
from rest_framework.fields import CharField
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
from authentik.sources.oauth.clients.oauth2 import OAuth2Client
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -13,18 +18,34 @@ from authentik.sources.oauth.views.redirect import OAuthRedirect
LOGGER = get_logger()
class AppleLoginChallenge(Challenge):
"""Special challenge for apple-native authentication flow, which happens on the client."""
client_id = CharField()
component = CharField(default="ak-flow-sources-oauth-apple")
scope = CharField()
redirect_uri = CharField()
state = CharField()
class AppleChallengeResponse(ChallengeResponse):
"""Pseudo class for plex response"""
component = CharField(default="ak-flow-sources-oauth-apple")
class AppleOAuthClient(OAuth2Client):
"""Apple OAuth2 client"""
def get_client_id(self) -> str:
parts = self.source.consumer_key.split(";")
parts: list[str] = self.source.consumer_key.split(";")
if len(parts) < 3:
return self.source.consumer_key
return parts[0]
return parts[0].strip()
def get_client_secret(self) -> str:
now = time()
parts = self.source.consumer_key.split(";")
parts: list[str] = self.source.consumer_key.split(";")
if len(parts) < 3:
raise ValueError(
(
@ -34,14 +55,14 @@ class AppleOAuthClient(OAuth2Client):
)
LOGGER.debug("got values from client_id", team=parts[1], kid=parts[2])
payload = {
"iss": parts[1],
"iss": parts[1].strip(),
"iat": now,
"exp": now + 86400 * 180,
"aud": "https://appleid.apple.com",
"sub": parts[0],
"sub": parts[0].strip(),
}
# pyright: reportGeneralTypeIssues=false
jwt = encode(payload, self.source.consumer_secret, "ES256", {"kid": parts[2]})
jwt = encode(payload, self.source.consumer_secret, "ES256", {"kid": parts[2].strip()})
LOGGER.debug("signing payload as secret key", payload=payload, jwt=jwt)
return jwt
@ -55,7 +76,7 @@ class AppleOAuthRedirect(OAuthRedirect):
client_class = AppleOAuthClient
def get_additional_parameters(self, source): # pragma: no cover
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": "name email",
"response_mode": "form_post",
@ -74,7 +95,6 @@ class AppleOAuth2Callback(OAuthCallback):
self,
info: dict[str, Any],
) -> dict[str, Any]:
print(info)
return {
"email": info.get("email"),
"name": info.get("name"),
@ -96,3 +116,24 @@ class AppleType(SourceType):
def icon_url(self) -> str:
return "https://appleid.cdn-apple.com/appleid/button/logo"
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
"""Pre-general all the things required for the JS SDK"""
apple_client = AppleOAuthClient(
source,
request,
callback=reverse(
"authentik_sources_oauth:oauth-client-callback",
kwargs={"source_slug": source.slug},
),
)
args = apple_client.get_redirect_args()
return AppleLoginChallenge(
instance={
"client_id": apple_client.get_client_id(),
"scope": "name email",
"redirect_uri": args["redirect_uri"],
"state": args["state"],
"type": ChallengeTypes.NATIVE.value,
}
)

View file

@ -2,9 +2,13 @@
from enum import Enum
from typing import Callable, Optional, Type
from django.http.request import HttpRequest
from django.templatetags.static import static
from django.urls.base import reverse
from structlog.stdlib import get_logger
from authentik.flows.challenge import Challenge, ChallengeTypes, RedirectChallenge
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
@ -37,6 +41,19 @@ class SourceType:
"""Get Icon URL for login"""
return static(f"authentik/sources/{self.slug}.svg")
# pylint: disable=unused-argument
def login_challenge(self, source: OAuthSource, request: HttpRequest) -> Challenge:
"""Allow types to return custom challenges"""
return RedirectChallenge(
instance={
"type": ChallengeTypes.REDIRECT.value,
"to": reverse(
"authentik_sources_oauth:oauth-client-login",
kwargs={"source_slug": source.slug},
),
}
)
class SourceTypeManager:
"""Manager to hold all Source types."""

View file

@ -0,0 +1,51 @@
"""Okta OAuth Views"""
from typing import Any
from authentik.sources.oauth.models import OAuthSource
from authentik.sources.oauth.types.azure_ad import AzureADClient
from authentik.sources.oauth.types.manager import MANAGER, SourceType
from authentik.sources.oauth.views.callback import OAuthCallback
from authentik.sources.oauth.views.redirect import OAuthRedirect
class OktaOAuthRedirect(OAuthRedirect):
"""Okta OAuth2 Redirect"""
def get_additional_parameters(self, source: OAuthSource): # pragma: no cover
return {
"scope": "openid email profile",
}
class OktaOAuth2Callback(OAuthCallback):
"""Okta OAuth2 Callback"""
# Okta has the same quirk as azure and throws an error if the access token
# is set via query parameter, so we re-use the azure client
# see https://github.com/goauthentik/authentik/issues/1910
client_class = AzureADClient
def get_user_id(self, info: dict[str, str]) -> str:
return info.get("sub", "")
def get_user_enroll_context(
self,
info: dict[str, Any],
) -> dict[str, Any]:
return {
"username": info.get("nickname"),
"email": info.get("email"),
"name": info.get("name"),
}
@MANAGER.type()
class OktaType(SourceType):
"""Okta Type definition"""
callback_view = OktaOAuth2Callback
redirect_view = OktaOAuthRedirect
name = "Okta"
slug = "okta"
urls_customizable = True

View file

@ -3,6 +3,7 @@ from typing import Optional
from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.http.request import HttpRequest
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from rest_framework.fields import CharField
@ -62,8 +63,7 @@ class PlexSource(Source):
return PlexSourceSerializer
@property
def ui_login_button(self) -> UILoginButton:
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
return UILoginButton(
challenge=PlexAuthenticationChallenge(
{
@ -77,7 +77,6 @@ class PlexSource(Source):
name=self.name,
)
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View file

@ -167,8 +167,7 @@ class SAMLSource(Source):
reverse(f"authentik_sources_saml:{view}", kwargs={"source_slug": self.slug})
)
@property
def ui_login_button(self) -> UILoginButton:
def ui_login_button(self, request: HttpRequest) -> UILoginButton:
return UILoginButton(
challenge=RedirectChallenge(
instance={

View file

@ -48,7 +48,6 @@ class AuthenticatorDuoStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-duo-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View file

@ -141,7 +141,6 @@ class AuthenticatorSMSStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-sms-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View file

@ -90,6 +90,5 @@ class AuthenticatorSMSStageTests(APITestCase):
"code": int(self.client.session[SESSION_SMS_DEVICE].token),
},
)
print(response.content)
self.assertEqual(response.status_code, 200)
sms_send_mock.assert_not_called()

View file

@ -31,7 +31,6 @@ class AuthenticatorStaticStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-static-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View file

@ -38,7 +38,6 @@ class AuthenticatorTOTPStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-totp-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View file

@ -18,7 +18,7 @@ class AuthenticateWebAuthnStageSerializer(StageSerializer):
class Meta:
model = AuthenticateWebAuthnStage
fields = StageSerializer.Meta.fields + ["configure_flow"]
fields = StageSerializer.Meta.fields + ["configure_flow", "user_verification"]
class AuthenticateWebAuthnStageViewSet(UsedByMixin, ModelViewSet):

View file

@ -0,0 +1,25 @@
# Generated by Django 4.0 on 2021-12-14 09:05
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("authentik_stages_authenticator_webauthn", "0004_auto_20210304_1850"),
]
operations = [
migrations.AddField(
model_name="authenticatewebauthnstage",
name="user_verification",
field=models.TextField(
choices=[
("required", "Required"),
("preferred", "Preferred"),
("discouraged", "Discouraged"),
],
default="preferred",
),
),
]

View file

@ -15,9 +15,30 @@ from authentik.core.types import UserSettingSerializer
from authentik.flows.models import ConfigurableStage, Stage
class UserVerification(models.TextChoices):
"""The degree to which the Relying Party wishes to verify a user's identity.
Members:
`REQUIRED`: User verification must occur
`PREFERRED`: User verification would be great, but if not that's okay too
`DISCOURAGED`: User verification should not occur, but it's okay if it does
https://www.w3.org/TR/webauthn-2/#enumdef-userverificationrequirement
"""
REQUIRED = "required"
PREFERRED = "preferred"
DISCOURAGED = "discouraged"
class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
"""WebAuthn stage"""
user_verification = models.TextField(
choices=UserVerification.choices,
default=UserVerification.PREFERRED,
)
@property
def serializer(self) -> BaseSerializer:
from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageSerializer
@ -34,7 +55,6 @@ class AuthenticateWebAuthnStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-authenticator-webauthn-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
return UserSettingSerializer(
data={

View file

@ -14,7 +14,6 @@ from webauthn.helpers.structs import (
PublicKeyCredentialCreationOptions,
RegistrationCredential,
ResidentKeyRequirement,
UserVerificationRequirement,
)
from webauthn.registration.verify_registration_response import VerifiedRegistration
@ -27,7 +26,7 @@ from authentik.flows.challenge import (
)
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER
from authentik.flows.stage import ChallengeStageView
from authentik.stages.authenticator_webauthn.models import WebAuthnDevice
from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage, WebAuthnDevice
from authentik.stages.authenticator_webauthn.utils import get_origin, get_rp_id
LOGGER = get_logger()
@ -83,7 +82,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
def get_challenge(self, *args, **kwargs) -> Challenge:
# clear session variables prior to starting a new registration
self.request.session.pop("challenge", None)
stage: AuthenticateWebAuthnStage = self.executor.current_stage
user = self.get_pending_user()
registration_options: PublicKeyCredentialCreationOptions = generate_registration_options(
@ -94,10 +93,9 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView):
user_display_name=user.name,
authenticator_selection=AuthenticatorSelectionCriteria(
resident_key=ResidentKeyRequirement.PREFERRED,
user_verification=UserVerificationRequirement.PREFERRED,
user_verification=str(stage.user_verification),
),
)
registration_options.user.id = user.uid
self.request.session["challenge"] = registration_options.challenge
return AuthenticatorWebAuthnChallenge(

View file

@ -29,4 +29,4 @@ class TestEmailStageAPI(APITestCase):
EmailTemplates.ACCOUNT_CONFIRM,
)
with self.assertRaises(ValidationError):
print(EmailStageSerializer().validate_template("foobar"))
EmailStageSerializer().validate_template("foobar")

View file

@ -12,6 +12,7 @@ from django.utils.translation import gettext as _
from drf_spectacular.utils import PolymorphicProxySerializer, extend_schema_field
from rest_framework.fields import BooleanField, CharField, DictField, ListField
from rest_framework.serializers import ValidationError
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.api.utils import PassiveSerializer
@ -25,6 +26,7 @@ from authentik.flows.challenge import (
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
from authentik.sources.oauth.types.apple import AppleLoginChallenge
from authentik.sources.plex.models import PlexAuthenticationChallenge
from authentik.stages.identification.models import IdentificationStage
from authentik.stages.identification.signals import identification_failed
@ -39,6 +41,7 @@ LOGGER = get_logger()
serializers={
RedirectChallenge().fields["component"].default: RedirectChallenge,
PlexAuthenticationChallenge().fields["component"].default: PlexAuthenticationChallenge,
AppleLoginChallenge().fields["component"].default: AppleLoginChallenge,
},
resource_type_field_name="component",
)
@ -88,8 +91,12 @@ class IdentificationChallengeResponse(ChallengeResponse):
pre_user = self.stage.get_user(uid_field)
if not pre_user:
# Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
sleep(0.30 * SystemRandom().randint(3, 7))
with Hub.current.start_span(
op="authentik.stages.identification.validate_invalid_wait",
description="Sleep random time on invalid user identifier",
):
# Sleep a random time (between 90 and 210ms) to "prevent" user enumeration attacks
sleep(0.030 * 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
@ -112,12 +119,16 @@ class IdentificationChallengeResponse(ChallengeResponse):
if not password:
LOGGER.warning("Password not set for ident+auth attempt")
try:
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
username=self.pre_user.username,
password=password,
)
with Hub.current.start_span(
op="authentik.stages.identification.authenticate",
description="User authenticate call (combo stage)",
):
user = authenticate(
self.stage.request,
current_stage.password_stage.backends,
username=self.pre_user.username,
password=password,
)
if not user:
raise ValidationError("Failed to authenticate.")
self.pre_user = user
@ -191,7 +202,7 @@ class IdentificationStageView(ChallengeStageView):
current_stage.sources.filter(enabled=True).order_by("name").select_subclasses()
)
for source in sources:
ui_login_button = source.ui_login_button
ui_login_button = source.ui_login_button(self.request)
if ui_login_button:
button = asdict(ui_login_button)
button["challenge"] = ui_login_button.challenge.data

View file

@ -5,8 +5,8 @@ from rest_framework.fields import JSONField
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from authentik.core.api.groups import GroupMemberSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.core.api.users import UserSerializer
from authentik.core.api.utils import is_dict
from authentik.flows.api.stages import StageSerializer
from authentik.stages.invitation.models import Invitation, InvitationStage
@ -46,7 +46,7 @@ class InvitationStageViewSet(UsedByMixin, ModelViewSet):
class InvitationSerializer(ModelSerializer):
"""Invitation Serializer"""
created_by = UserSerializer(read_only=True)
created_by = GroupMemberSerializer(read_only=True)
fixed_data = JSONField(validators=[is_dict], required=False)
class Meta:

View file

@ -63,7 +63,6 @@ class PasswordStage(ConfigurableStage, Stage):
def component(self) -> str:
return "ak-stage-password-form"
@property
def ui_user_settings(self) -> Optional[UserSettingSerializer]:
if not self.configure_flow:
return None

View file

@ -10,6 +10,7 @@ from django.urls import reverse
from django.utils.translation import gettext as _
from rest_framework.exceptions import ErrorDetail, ValidationError
from rest_framework.fields import CharField
from sentry_sdk.hub import Hub
from structlog.stdlib import get_logger
from authentik.core.models import User
@ -43,7 +44,11 @@ def authenticate(request: HttpRequest, backends: list[str], **credentials: Any)
LOGGER.warning("Failed to import backend", path=backend_path)
continue
LOGGER.debug("Attempting authentication...", backend=backend_path)
user = backend.authenticate(request, **credentials)
with Hub.current.start_span(
op="authentik.stages.password.authenticate",
description=backend_path,
):
user = backend.authenticate(request, **credentials)
if user is None:
LOGGER.debug("Backend returned nothing, continuing", backend=backend_path)
continue
@ -120,7 +125,13 @@ class PasswordStageView(ChallengeStageView):
"username": pending_user.username,
}
try:
user = authenticate(self.request, self.executor.current_stage.backends, **auth_kwargs)
with Hub.current.start_span(
op="authentik.stages.password.authenticate",
description="User authenticate call",
):
user = authenticate(
self.request, self.executor.current_stage.backends, **auth_kwargs
)
except PermissionDenied:
del auth_kwargs["password"]
# User was found, but permission was denied (i.e. user is not active)

View file

@ -4,6 +4,7 @@ from typing import Any
from django.db.models import F, Q
from django.db.models import Value as V
from django.http.request import HttpRequest
from sentry_sdk.hub import Hub
from authentik.lib.config import CONFIG
from authentik.tenants.models import Tenant
@ -28,7 +29,12 @@ def get_tenant_for_request(request: HttpRequest) -> Tenant:
def context_processor(request: HttpRequest) -> dict[str, Any]:
"""Context Processor that injects tenant object into every template"""
tenant = getattr(request, "tenant", DEFAULT_TENANT)
trace = ""
span = Hub.current.scope.span
if span:
trace = span.to_traceparent()
return {
"tenant": tenant,
"footer_links": CONFIG.y("footer_links"),
"sentry_trace": trace,
}

2
go.mod
View file

@ -28,7 +28,7 @@ require (
github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac // indirect
github.com/prometheus/client_golang v1.11.0
github.com/sirupsen/logrus v1.8.1
goauthentik.io/api v0.2021104.11
goauthentik.io/api v0.2021104.17
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

4
go.sum
View file

@ -558,8 +558,8 @@ 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.2021104.11 h1:LqT0LM0e/RRrxPuo6Xl5uz3PCR5ytuE+YlNlfW9w0yU=
goauthentik.io/api v0.2021104.11/go.mod h1:02nnD4FRd8lu8A1+ZuzqownBgvAhdCKzqkKX8v7JMTE=
goauthentik.io/api v0.2021104.17 h1:NnfdoIlAekwPu+G7h7X/SGbWjWSypEy/pGQDD7/J+Vw=
goauthentik.io/api v0.2021104.17/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=

View file

@ -47,7 +47,7 @@ type APIController struct {
// NewAPIController initialise new API Controller instance from URL and API token
func NewAPIController(akURL url.URL, token string) *APIController {
rsp := sentry.StartSpan(context.TODO(), "authentik.outposts.init")
rsp := sentry.StartSpan(context.Background(), "authentik.outposts.init")
config := api.NewConfiguration()
config.Host = akURL.Host

View file

@ -107,6 +107,7 @@ func (ac *APIController) reconnectWS() {
}
} else {
ac.wsIsReconnecting = false
ac.wsBackoffMultiplier = 1
return
}
}

View file

@ -2,6 +2,7 @@ package ak
import (
"context"
"fmt"
"net/http"
"github.com/getsentry/sentry-go"
@ -19,6 +20,8 @@ func NewTracingTransport(ctx context.Context, inner http.RoundTripper) *tracingT
func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
span := sentry.StartSpan(tt.ctx, "authentik.go.http_request")
r.Header.Set("sentry-trace", span.ToSentryTrace())
span.Description = fmt.Sprintf("%s %s", r.Method, r.URL.String())
span.SetTag("url", r.URL.String())
span.SetTag("method", r.Method)
defer span.Finish()

View file

@ -168,6 +168,7 @@ func NewApplication(p api.ProxyOutpostConfig, c *http.Client, cs *ak.CryptoStore
func (a *Application) IsAllowlisted(r *http.Request) bool {
for _, u := range a.UnauthenticatedRegex {
a.log.WithField("regex", u.String()).WithField("url", r.URL.Path).Trace("Matching URL against allow list")
if u.MatchString(r.URL.Path) {
return true
}

View file

@ -2,6 +2,7 @@ package application
import (
"fmt"
"math"
"os"
"strconv"
@ -18,6 +19,7 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
if err != nil {
panic(err)
}
rs.SetMaxLength(math.MaxInt64)
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length
@ -27,14 +29,22 @@ func (a *Application) getStore(p api.ProxyOutpostConfig) sessions.Store {
a.log.Info("using redis session backend")
store = rs
} else {
cs := sessions.NewFilesystemStore(os.TempDir(), []byte(*p.CookieSecret))
dir := os.TempDir()
cs := sessions.NewFilesystemStore(dir, []byte(*p.CookieSecret))
cs.Options.Domain = *p.CookieDomain
// https://github.com/markbates/goth/commit/7276be0fdf719ddff753f3574ef0f967e4a5a5f7
// set the maxLength of the cookies stored on the disk to a larger number to prevent issues with:
// securecookie: the value is too long
// when using OpenID Connect , since this can contain a large amount of extra information in the id_token
// Note, when using the FilesystemStore only the session.ID is written to a browser cookie, so this is explicit for the storage on disk
cs.MaxLength(math.MaxInt64)
if p.TokenValidity.IsSet() {
t := p.TokenValidity.Get()
// Add one to the validity to ensure we don't have a session with indefinite length
cs.Options.MaxAge = int(*t) + 1
}
a.log.Info("using filesystem session backend")
a.log.WithField("dir", dir).Info("using filesystem session backend")
store = cs
}
return store

File diff suppressed because it is too large Load diff

View file

@ -3909,6 +3909,36 @@ paths:
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/events/events/per_month/:
get:
operationId: events_events_per_month_list
description: Get the count of events per month
parameters:
- in: query
name: action
schema:
type: string
- in: query
name: query
schema:
type: string
tags:
- events
security:
- authentik: []
responses:
'200':
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Coordinate'
description: ''
'400':
$ref: '#/components/schemas/ValidationError'
'403':
$ref: '#/components/schemas/GenericError'
/events/events/top_per_user/:
get:
operationId: events_events_top_per_user_list
@ -3918,56 +3948,10 @@ paths:
name: action
schema:
type: string
- in: query
name: client_ip
schema:
type: string
- in: query
name: context_authorized_app
schema:
type: string
description: Context Authorized application
- in: query
name: context_model_app
schema:
type: string
description: Context Model App
- in: query
name: context_model_name
schema:
type: string
description: Context Model Name
- in: query
name: context_model_pk
schema:
type: string
description: Context Model Primary Key
- name: ordering
required: false
in: query
description: Which field to use when ordering the results.
schema:
type: string
- name: search
required: false
in: query
description: A search term.
schema:
type: string
- in: query
name: tenant_name
schema:
type: string
description: Tenant name
- in: query
name: top_n
schema:
type: integer
- in: query
name: username
schema:
type: string
description: Username
tags:
- events
security:
@ -7539,6 +7523,7 @@ paths:
- configuration_error
- custom_
- email_sent
- flow_execution
- impersonation_ended
- impersonation_started
- invitation_used
@ -15395,6 +15380,14 @@ paths:
schema:
type: string
format: uuid
- in: query
name: user_verification
schema:
type: string
enum:
- discouraged
- preferred
- required
tags:
- stages
security:
@ -19086,6 +19079,46 @@ components:
- authentik.managed
- authentik.core
type: string
AppleChallengeResponseRequest:
type: object
description: Pseudo class for plex response
properties:
component:
type: string
minLength: 1
default: ak-flow-sources-oauth-apple
AppleLoginChallenge:
type: object
description: Special challenge for apple-native authentication flow, which happens
on the client.
properties:
type:
$ref: '#/components/schemas/ChallengeChoices'
flow_info:
$ref: '#/components/schemas/ContextualFlowInfo'
component:
type: string
default: ak-flow-sources-oauth-apple
response_errors:
type: object
additionalProperties:
type: array
items:
$ref: '#/components/schemas/ErrorDetail'
client_id:
type: string
scope:
type: string
redirect_uri:
type: string
state:
type: string
required:
- client_id
- redirect_uri
- scope
- state
- type
Application:
type: object
description: Application Serializer
@ -19200,6 +19233,8 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
required:
- component
- meta_model_name
@ -19224,6 +19259,8 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
required:
- name
AuthenticatedSession:
@ -20225,6 +20262,7 @@ components:
ChallengeTypes:
oneOf:
- $ref: '#/components/schemas/AccessDeniedChallenge'
- $ref: '#/components/schemas/AppleLoginChallenge'
- $ref: '#/components/schemas/AuthenticatorDuoChallenge'
- $ref: '#/components/schemas/AuthenticatorSMSChallenge'
- $ref: '#/components/schemas/AuthenticatorStaticChallenge'
@ -20246,6 +20284,7 @@ components:
propertyName: component
mapping:
ak-stage-access-denied: '#/components/schemas/AccessDeniedChallenge'
ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge'
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallenge'
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallenge'
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallenge'
@ -21080,6 +21119,7 @@ components:
- source_linked
- impersonation_started
- impersonation_ended
- flow_execution
- policy_execution
- policy_exception
- property_mapping_exception
@ -21387,6 +21427,7 @@ components:
- title
FlowChallengeResponseRequest:
oneOf:
- $ref: '#/components/schemas/AppleChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
- $ref: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
@ -21405,6 +21446,7 @@ components:
discriminator:
propertyName: component
mapping:
ak-flow-sources-oauth-apple: '#/components/schemas/AppleChallengeResponseRequest'
ak-stage-authenticator-duo: '#/components/schemas/AuthenticatorDuoChallengeResponseRequest'
ak-stage-authenticator-sms: '#/components/schemas/AuthenticatorSMSChallengeResponseRequest'
ak-stage-authenticator-static: '#/components/schemas/AuthenticatorStaticChallengeResponseRequest'
@ -22071,7 +22113,7 @@ components:
additionalProperties: {}
created_by:
allOf:
- $ref: '#/components/schemas/User'
- $ref: '#/components/schemas/GroupMember'
readOnly: true
single_use:
type: boolean
@ -22711,11 +22753,13 @@ components:
oneOf:
- $ref: '#/components/schemas/RedirectChallenge'
- $ref: '#/components/schemas/PlexAuthenticationChallenge'
- $ref: '#/components/schemas/AppleLoginChallenge'
discriminator:
propertyName: component
mapping:
xak-flow-redirect: '#/components/schemas/RedirectChallenge'
ak-flow-sources-plex: '#/components/schemas/PlexAuthenticationChallenge'
ak-flow-sources-oauth-apple: '#/components/schemas/AppleLoginChallenge'
LoginMetrics:
type: object
description: Login Metrics per 1h
@ -26565,6 +26609,8 @@ components:
nullable: true
description: Flow used by an authenticated user to configure this Stage.
If empty, user will not be able to configure this stage.
user_verification:
$ref: '#/components/schemas/UserVerificationEnum'
PatchedAuthenticatorDuoStageRequest:
type: object
description: AuthenticatorDuoStage Serializer
@ -28992,6 +29038,7 @@ components:
- github
- google
- openidconnect
- okta
- reddit
- twitter
type: string
@ -31188,6 +31235,12 @@ components:
- pk
- source
- user
UserVerificationEnum:
enum:
- required
- preferred
- discouraged
type: string
UserWriteStage:
type: object
description: UserWriteStage Serializer

View file

@ -40,7 +40,7 @@ class TestProviderOAuth2OIDC(SeleniumTestCase):
sleep(1)
client: DockerClient = from_env()
container = client.containers.run(
image="beryju.org/oidc-test-client:latest",
image="ghcr.io/beryju/oidc-test-client:latest",
detach=True,
network_mode="host",
auto_remove=True,

View file

@ -40,7 +40,7 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
sleep(1)
client: DockerClient = from_env()
container = client.containers.run(
image="beryju.org/oidc-test-client:latest",
image="ghcr.io/beryju/oidc-test-client:latest",
detach=True,
network_mode="host",
auto_remove=True,
@ -145,7 +145,6 @@ class TestProviderOAuth2OIDCImplicit(SeleniumTestCase):
self.wait.until(ec.presence_of_element_located((By.CSS_SELECTOR, "pre")))
sleep(1)
body = loads(self.driver.find_element(By.CSS_SELECTOR, "pre").text)
print(body)
self.assertEqual(body["profile"]["nickname"], self.user.username)
self.assertEqual(body["profile"]["name"], self.user.name)
self.assertEqual(body["profile"]["email"], self.user.email)

View file

@ -39,7 +39,7 @@ class TestProviderSAML(SeleniumTestCase):
if force_post:
metadata_url += f"&force_binding={SAML_BINDING_POST}"
container = client.containers.run(
image="beryju.org/saml-test-sp:latest",
image="ghcr.io/beryju/saml-test-sp:latest",
detach=True,
network_mode="host",
auto_remove=True,

View file

@ -229,7 +229,7 @@ class TestSourceOAuth1(SeleniumTestCase):
def get_container_specs(self) -> Optional[dict[str, Any]]:
return {
"image": "beryju.org/oauth1-test-server:latest",
"image": "ghcr.io/beryju/oauth1-test-server:latest",
"detach": True,
"network_mode": "host",
"auto_remove": True,

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 400 134.7" style="enable-background:new 0 0 400 134.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#007DC1;}
</style>
<g>
<g>
<g>
<path class="st0" d="M50.3,33.8C22.5,33.8,0,56.3,0,84.1s22.5,50.3,50.3,50.3s50.3-22.5,50.3-50.3S78.1,33.8,50.3,33.8z
M50.3,109.3c-13.9,0-25.2-11.3-25.2-25.2s11.3-25.2,25.2-25.2s25.2,11.3,25.2,25.2S64.2,109.3,50.3,109.3z"/>
</g>
<path class="st0" d="M138.7,101c0-4,4.8-5.9,7.6-3.1c12.6,12.8,33.4,34.8,33.5,34.9c0.3,0.3,0.6,0.8,1.8,1.2
c0.5,0.2,1.3,0.2,2.2,0.2l22.7,0c4.1,0,5.3-4.7,3.4-7.1l-37.6-38.5l-2-2c-4.3-5.1-3.8-7.1,1.1-12.3L201.2,41c1.9-2.4,0.7-7-3.5-7
h-20.6c-0.8,0-1.4,0-2,0.2c-1.2,0.4-1.7,0.8-2,1.2c-0.1,0.1-16.6,17.9-26.8,28.8c-2.8,3-7.8,1-7.8-3.1l0-57.1c0-2.9-2.4-4-4.3-4
h-16.8c-2.9,0-4.3,1.9-4.3,3.6v126.6c0,2.9,2.4,3.7,4.4,3.7h16.8c2.6,0,4.3-1.9,4.3-3.8v-1.3V101z"/>
<path class="st0" d="M275.9,129.6l-1.8-16.8c-0.2-2.3-2.4-3.9-4.7-3.5c-1.3,0.2-2.6,0.3-3.9,0.3c-13.4,0-24.3-10.5-25.1-23.8
c0-0.4,0-0.9,0-1.4V63.8c0-2.7,2-4.9,4.7-4.9l22.5,0c1.6,0,4-1.4,4-4.3V38.7c0-3.1-2-4.7-3.8-4.7h-22.7c-2.6,0-4.7-1.9-4.8-4.5
l0-25.5c0-1.6-1.2-4-4.3-4h-16.7c-2.1,0-4.1,1.3-4.1,3.9c0,0,0,81.5,0,81.9c0.7,27.2,23,48.9,50.3,48.9c2.3,0,4.5-0.2,6.7-0.5
C274.6,133.9,276.2,131.9,275.9,129.6z"/>
</g>
<g>
<path class="st0" d="M397.1,108.5c-14.2,0-16.4-5.1-16.4-24.2c0-0.1,0-0.1,0-0.2l0-45.9c0-1.6-1.2-4.3-4.4-4.3h-16.8
c-2.1,0-4.4,1.7-4.4,4.3l0,2.1c-7.3-4.2-15.8-6.6-24.8-6.6c-27.8,0-50.3,22.5-50.3,50.3c0,27.8,22.5,50.3,50.3,50.3
c12.5,0,23.9-4.6,32.7-12.1c4.7,7.2,12.3,12,24.2,12.1c2,0,12.8,0.4,12.8-4.7v-17.9C400,110.2,398.8,108.5,397.1,108.5z
M330.4,109.3c-13.9,0-25.2-11.3-25.2-25.2c0-13.9,11.3-25.2,25.2-25.2c13.9,0,25.2,11.3,25.2,25.2
C355.5,98,344.2,109.3,330.4,109.3z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2 KiB

1545
web/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -45,24 +45,24 @@
]
},
"dependencies": {
"@babel/core": "^7.16.0",
"@babel/plugin-proposal-decorators": "^7.16.4",
"@babel/plugin-transform-runtime": "^7.16.4",
"@babel/preset-env": "^7.16.4",
"@babel/preset-typescript": "^7.16.0",
"@babel/core": "^7.16.5",
"@babel/plugin-proposal-decorators": "^7.16.5",
"@babel/plugin-transform-runtime": "^7.16.5",
"@babel/preset-env": "^7.16.5",
"@babel/preset-typescript": "^7.16.5",
"@fortawesome/fontawesome-free": "^5.15.4",
"@goauthentik/api": "^2021.10.4-1639076050",
"@goauthentik/api": "^2021.10.4-1639516687",
"@jackfranklin/rollup-plugin-markdown": "^0.3.0",
"@lingui/cli": "^3.13.0",
"@lingui/core": "^3.13.0",
"@lingui/detect-locale": "^3.13.0",
"@lingui/macro": "^3.13.0",
"@patternfly/patternfly": "^4.159.1",
"@patternfly/patternfly": "^4.164.2",
"@polymer/iron-form": "^3.0.1",
"@polymer/paper-input": "^3.2.1",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-commonjs": "^21.0.1",
"@rollup/plugin-node-resolve": "^13.0.6",
"@rollup/plugin-node-resolve": "^13.1.1",
"@rollup/plugin-replace": "^3.0.0",
"@rollup/plugin-typescript": "^8.3.0",
"@sentry/browser": "^6.16.1",
@ -72,8 +72,8 @@
"@types/chart.js": "^2.9.34",
"@types/codemirror": "5.60.5",
"@types/grecaptcha": "^3.0.3",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"@typescript-eslint/parser": "^5.7.0",
"@webcomponents/webcomponentsjs": "^2.6.0",
"babel-plugin-macros": "^3.1.0",
"base64-js": "^1.5.1",
@ -99,7 +99,7 @@
"rollup-plugin-terser": "^7.0.2",
"ts-lit-plugin": "^1.2.1",
"tslib": "^2.3.1",
"typescript": "^4.5.3",
"typescript": "^4.5.4",
"webcomponent-qr-code": "^1.0.5",
"yaml": "^1.10.2"
}

View file

@ -3,6 +3,7 @@ import { getCookie } from "../utils";
import { APIMiddleware } from "../elements/notifications/APIDrawer";
import { MessageMiddleware } from "../elements/messages/Middleware";
import { VERSION } from "../constants";
import { getMetaContent } from "@sentry/tracing/dist/browser/browsertracing";
export class LoggingMiddleware implements Middleware {
@ -53,6 +54,7 @@ export const DEFAULT_CONFIG = new Configuration({
basePath: process.env.AK_API_BASE_PATH + "/api/v3",
headers: {
"X-CSRFToken": getCookie("authentik_csrf"),
"sentry-trace": getMetaContent("sentry-trace") || "",
},
middleware: [
new APIMiddleware(),

View file

@ -27,7 +27,10 @@ export function configureSentry(canDoPpi: boolean = false): Promise<Config> {
],
tracesSampleRate: config.errorReporting.tracesSampleRate,
environment: config.errorReporting.environment,
beforeSend: async (event: Sentry.Event, hint: Sentry.EventHint): Promise<Sentry.Event | null> => {
beforeSend: async (event: Sentry.Event, hint: Sentry.EventHint | undefined): Promise<Sentry.Event | null> => {
if (!hint) {
return event;
}
if (hint.originalException instanceof SentryIgnoredError) {
return null;
}
@ -40,8 +43,13 @@ export function configureSentry(canDoPpi: boolean = false): Promise<Config> {
Sentry.setTag(TAG_SENTRY_CAPABILITIES, config.capabilities.join(","));
if (window.location.pathname.includes("if/")) {
// Get the interface name from URL
const intf = window.location.pathname.replace(/.+if\/(.+)\//, "$1");
Sentry.setTag(TAG_SENTRY_COMPONENT, `web/${intf}`);
const pathMatches = window.location.pathname.match(/.+if\/(\w+)\//);
let currentInterface = "unknown";
if (pathMatches && pathMatches.length >= 2) {
currentInterface = pathMatches[1];
}
Sentry.setTag(TAG_SENTRY_COMPONENT, `web/${currentInterface}`);
Sentry.configureScope((scope) => scope.setTransactionName(`authentik.web.if.${currentInterface}`));
}
if (config.errorReporting.sendPii && canDoPpi) {
me().then(user => {

View file

@ -108,10 +108,11 @@ export class PageHeader extends LitElement {
renderIcon(): TemplateResult {
if (this.icon) {
if (this.iconImage) {
if (this.iconImage && !this.icon.startsWith("fa://")) {
return html`<img class="pf-icon" src="${this.icon}" />&nbsp;`;
}
return html`<i class=${this.icon}></i>&nbsp;`;
const icon = this.icon.replaceAll("fa://", "fa ");
return html`<i class=${icon}></i>&nbsp;`;
}
return html``;
}
@ -132,7 +133,10 @@ export class PageHeader extends LitElement {
</button>
<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>${this.renderIcon()} ${this.header}</h1>
<h1>
${this.renderIcon()}
<slot name="header"> ${this.header} </slot>
</h1>
${this.description ? html`<p>${this.description}</p>` : html``}
</div>
</section>

View file

@ -18,6 +18,9 @@ export class AggregateCard extends LitElement {
@property()
headerLink?: string;
@property({ type: Boolean })
isCenter = true;
static get styles(): CSSResult[] {
return [PFBase, PFCard, PFFlex, AKGlobal].concat([
css`
@ -59,7 +62,9 @@ export class AggregateCard extends LitElement {
</div>
${this.renderHeaderLink()}
</div>
<div class="pf-c-card__body center-value">${this.renderInner()}</div>
<div class="pf-c-card__body ${this.isCenter ? "center-value" : ""}">
${this.renderInner()}
</div>
</div>`;
}
}

View file

@ -0,0 +1,52 @@
import { ChartData, Tick } from "chart.js";
import { t } from "@lingui/macro";
import { customElement, property } from "lit/decorators.js";
import { Coordinate, EventActions, EventsApi } from "@goauthentik/api";
import { DEFAULT_CONFIG } from "../../api/Config";
import { AKChart } from "./Chart";
@customElement("ak-charts-admin-model-per-day")
export class AdminModelPerDay extends AKChart<Coordinate[]> {
@property()
action: EventActions = EventActions.ModelCreated;
@property({ attribute: false })
query?: { [key: string]: unknown } | undefined;
apiRequest(): Promise<Coordinate[]> {
return new EventsApi(DEFAULT_CONFIG).eventsEventsPerMonthList({
action: this.action,
query: JSON.stringify(this.query || {}),
});
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600 / 24);
return t`${ago} days ago`;
}
getChartData(data: Coordinate[]): ChartData {
return {
datasets: [
{
label: t`Objects created`,
backgroundColor: "rgba(189, 229, 184, .5)",
spanGaps: true,
data:
data.map((cord) => {
return {
x: cord.xCord || 0,
y: cord.yCord || 0,
};
}) || [],
},
],
};
}
}

View file

@ -5,6 +5,8 @@ import { ArcElement, BarElement } from "chart.js";
import { LinearScale, TimeScale } from "chart.js";
import "chartjs-adapter-moment";
import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, css, html } from "lit";
import { property } from "lit/decorators.js";
@ -114,6 +116,13 @@ export abstract class AKChart<T> extends LitElement {
];
}
timeTickCallback(tickValue: string | number, index: number, ticks: Tick[]): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return t`${ago} hours ago`;
}
getOptions(): ChartOptions {
return {
maintainAspectRatio: false,
@ -122,15 +131,8 @@ export abstract class AKChart<T> extends LitElement {
type: "time",
display: true,
ticks: {
callback: function (
tickValue: string | number,
index: number,
ticks: Tick[],
): string {
const valueStamp = ticks[index];
const delta = Date.now() - valueStamp.value;
const ago = Math.round(delta / 1000 / 3600);
return `${ago} Hours ago`;
callback: (tickValue: string | number, index: number, ticks: Tick[]) => {
return this.timeTickCallback(tickValue, index, ticks);
},
autoSkip: true,
maxTicksLimit: 8,

View file

@ -187,6 +187,7 @@ export class DeleteBulkForm extends ModalButton {
<p class="pf-c-title">
${t`Are you sure you want to delete ${this.objects.length} ${this.objectLabel}?`}
</p>
<slot name="notice"></slot>
</form>
</section>
<section class="pf-c-page__main-section">

View file

@ -102,6 +102,7 @@ export class APIDrawer extends LitElement {
<div class="pf-c-notification-drawer__header">
<div class="text">
<h1 class="pf-c-notification-drawer__header-title">${t`API Requests`}</h1>
<a href="/api/v3/" target="_blank">${t`Open API Browser`}</a>
</div>
<div class="pf-c-notification-drawer__header-action">
<div class="pf-c-notification-drawer__header-action-close">

View file

@ -30,6 +30,19 @@ window.addEventListener("load", () => {
})();
});
export function paramURL(url: string, params?: { [key: string]: unknown }): string {
let finalUrl = "#";
finalUrl += url;
if (params) {
finalUrl += ";";
finalUrl += encodeURIComponent(JSON.stringify(params));
}
return finalUrl;
}
export function navigate(url: string, params?: { [key: string]: unknown }): void {
window.location.assign(paramURL(url, params));
}
@customElement("ak-router-outlet")
export class RouterOutlet extends LitElement {
@property({ attribute: false })

View file

@ -32,6 +32,7 @@ import "../elements/LoadingOverlay";
import { first } from "../utils";
import "./FlowInspector";
import "./access_denied/FlowAccessDenied";
import "./sources/apple/AppleLoginInit";
import "./sources/plex/PlexLoginInit";
import "./stages/RedirectStage";
import "./stages/authenticator_duo/AuthenticatorDuoStage";
@ -321,6 +322,11 @@ export class FlowExecutor extends LitElement implements StageHost {
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-plex>`;
case "ak-flow-sources-oauth-apple":
return html`<ak-flow-sources-oauth-apple
.host=${this as StageHost}
.challenge=${this.challenge}
></ak-flow-sources-oauth-apple>`;
default:
break;
}

View file

@ -0,0 +1,79 @@
import { t } from "@lingui/macro";
import { CSSResult, TemplateResult, html } from "lit";
import { customElement, property } from "lit/decorators.js";
import AKGlobal from "../../../authentik.css";
import PFButton from "@patternfly/patternfly/components/Button/button.css";
import PFForm from "@patternfly/patternfly/components/Form/form.css";
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
import PFLogin from "@patternfly/patternfly/components/Login/login.css";
import PFTitle from "@patternfly/patternfly/components/Title/title.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
import { AppleChallengeResponseRequest, AppleLoginChallenge } from "@goauthentik/api";
import "../../../elements/EmptyState";
import { BaseStage } from "../../stages/base";
@customElement("ak-flow-sources-oauth-apple")
export class AppleLoginInit extends BaseStage<AppleLoginChallenge, AppleChallengeResponseRequest> {
@property({ type: Boolean })
isModalShown = false;
static get styles(): CSSResult[] {
return [PFBase, PFLogin, PFForm, PFFormControl, PFButton, PFTitle, AKGlobal];
}
firstUpdated(): void {
const appleAuth = document.createElement("script");
appleAuth.src =
"https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js";
appleAuth.type = "text/javascript";
appleAuth.onload = () => {
AppleID.auth.init({
clientId: this.challenge?.clientId,
scope: this.challenge.scope,
redirectURI: this.challenge.redirectUri,
state: this.challenge.state,
usePopup: false,
});
AppleID.auth.signIn();
this.isModalShown = true;
};
document.head.append(appleAuth);
//Listen for authorization success
document.addEventListener("AppleIDSignInOnSuccess", () => {
//handle successful response
});
//Listen for authorization failures
document.addEventListener("AppleIDSignInOnFailure", (error) => {
console.warn(error);
this.isModalShown = false;
});
}
render(): TemplateResult {
return html`<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">${t`Authenticating with Apple...`}</h1>
</header>
<div class="pf-c-login__main-body">
<form class="pf-c-form">
<ak-empty-state ?loading="${true}"> </ak-empty-state>
${!this.isModalShown
? html`<button
class="pf-c-button pf-m-primary pf-m-block"
@click=${() => {
AppleID.auth.signIn();
}}
>
${t`Retry`}
</button>`
: html``}
</form>
</div>
<footer class="pf-c-login__main-footer">
<ul class="pf-c-login__main-footer-links"></ul>
</footer>`;
}
}

14
web/src/flows/sources/apple/apple.d.ts vendored Normal file
View file

@ -0,0 +1,14 @@
declare namespace AppleID {
const auth: AppleIDAuth;
class AppleIDAuth {
init({
clientId: string,
scope: string,
redirectURI: string,
state: string,
usePopup: boolean,
}): void;
async signIn(): Promise<void>;
}
}

View file

@ -51,6 +51,7 @@ export class WebAuthnAuthenticatorRegisterStage extends BaseStage<
// byte arrays as expected by the spec.
const publicKeyCredentialCreateOptions = transformCredentialCreateOptions(
this.challenge?.registration as PublicKeyCredentialCreationOptions,
this.challenge?.registration.user.id,
);
// request the authenticator(s) to create a new credential keypair.

View file

@ -8,15 +8,26 @@ export function b64RawEnc(buf: Uint8Array): string {
return base64js.fromByteArray(buf).replace(/\+/g, "-").replace(/\//g, "_");
}
export function u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
/**
* Transforms items in the credentialCreateOptions generated on the server
* into byte arrays expected by the navigator.credentials.create() call
*/
export function transformCredentialCreateOptions(
credentialCreateOptions: PublicKeyCredentialCreationOptions,
userId: string,
): PublicKeyCredentialCreationOptions {
const user = credentialCreateOptions.user;
user.id = u8arr(b64enc(credentialCreateOptions.user.id as Uint8Array));
// Because json can't contain raw bytes, the server base64-encodes the User ID
// So to get the base64 encoded byte array, we first need to convert it to a regular
// string, then a byte array, re-encode it and wrap that in an array.
const stringId = decodeURIComponent(escape(window.atob(userId)));
user.id = u8arr(b64enc(u8arr(stringId)));
const challenge = u8arr(credentialCreateOptions.challenge.toString());
const transformedCredentialCreateOptions = Object.assign({}, credentialCreateOptions, {
@ -63,12 +74,6 @@ export function transformNewAssertionForServer(newAssertion: PublicKeyCredential
};
}
function u8arr(input: string): Uint8Array {
return Uint8Array.from(atob(input.replace(/_/g, "/").replace(/-/g, "+")), (c) =>
c.charCodeAt(0),
);
}
export function transformCredentialRequestOptions(
credentialRequestOptions: PublicKeyCredentialRequestOptions,
): PublicKeyCredentialRequestOptions {

View file

@ -189,43 +189,37 @@ export class AdminInterface extends LitElement {
<ak-sidebar-item path="/if/user/" ?isAbsoluteLink=${true} ?highlight=${true}>
<span slot="label">${t`User interface`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/administration/overview">
<span slot="label">${t`Overview`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/administration/system-tasks">
<span slot="label">${t`System Tasks`}</span>
<ak-sidebar-item .expanded=${true}>
<span slot="label">${t`Dashboards`}</span>
<ak-sidebar-item path="/administration/overview">
<span slot="label">${t`Overview`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/administration/dashboard/users">
<span slot="label">${t`Users`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/administration/system-tasks">
<span slot="label">${t`System Tasks`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
<span slot="label">${t`Resources`}</span>
<span slot="label">${t`Applications`}</span>
<ak-sidebar-item
path="/core/applications"
.activeWhen=${[`^/core/applications/(?<slug>${SLUG_REGEX})$`]}
>
<span slot="label">${t`Applications`}</span>
</ak-sidebar-item>
<ak-sidebar-item
path="/core/sources"
.activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]}
>
<span slot="label">${t`Sources`}</span>
</ak-sidebar-item>
<ak-sidebar-item
path="/core/providers"
.activeWhen=${[`^/core/providers/(?<id>${ID_REGEX})$`]}
>
<span slot="label">${t`Providers`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/core/tenants">
<span slot="label">${t`Tenants`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
<span slot="label">${t`Outposts`}</span>
<ak-sidebar-item path="/outpost/outposts">
<span slot="label">${t`Outposts`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/outpost/integrations">
<span slot="label">${t`Integrations`}</span>
<span slot="label">${t`Outpost Integrations`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
@ -272,12 +266,9 @@ export class AdminInterface extends LitElement {
<ak-sidebar-item path="/flow/stages/prompts">
<span slot="label">${t`Prompts`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/flow/stages/invitations">
<span slot="label">${t`Invitations`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
<span slot="label">${t`Identity & Cryptography`}</span>
<span slot="label">${t`Directory`}</span>
<ak-sidebar-item
path="/identity/users"
.activeWhen=${[`^/identity/users/(?<id>${ID_REGEX})$`]}
@ -287,12 +278,27 @@ export class AdminInterface extends LitElement {
<ak-sidebar-item path="/identity/groups">
<span slot="label">${t`Groups`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/crypto/certificates">
<span slot="label">${t`Certificates`}</span>
<ak-sidebar-item
path="/core/sources"
.activeWhen=${[`^/core/sources/(?<slug>${SLUG_REGEX})$`]}
>
<span slot="label">${t`Federation & Social login`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/core/tokens">
<span slot="label">${t`Tokens & App passwords`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/flow/stages/invitations">
<span slot="label">${t`Invitations`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
<ak-sidebar-item>
<span slot="label">${t`System`}</span>
<ak-sidebar-item path="/core/tenants">
<span slot="label">${t`Tenants`}</span>
</ak-sidebar-item>
<ak-sidebar-item path="/crypto/certificates">
<span slot="label">${t`Certificates`}</span>
</ak-sidebar-item>
</ak-sidebar-item>
`;
}

View file

@ -296,6 +296,10 @@ msgstr "Alternatively, if your current device has Duo installed, click on this l
msgid "Always require consent"
msgstr "Always require consent"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "An example setup can look like this:"
msgstr "An example setup can look like this:"
#: src/pages/stages/prompt/PromptForm.ts
msgid "Any HTML can be used."
msgstr "Any HTML can be used."
@ -344,6 +348,7 @@ msgstr "Application's display Name."
msgid "Application(s)"
msgstr "Application(s)"
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/applications/ApplicationListPage.ts
#: src/pages/outposts/OutpostForm.ts
@ -407,8 +412,12 @@ msgid "Assigned to application"
msgstr "Assigned to application"
#: src/pages/policies/PolicyListPage.ts
msgid "Assigned to {0} objects."
msgstr "Assigned to {0} objects."
msgid "Assigned to {0} object(s)."
msgstr "Assigned to {0} object(s)."
#: src/pages/policies/PolicyListPage.ts
#~ msgid "Assigned to {0} objects."
#~ msgstr "Assigned to {0} objects."
#: src/pages/events/EventInfo.ts
msgid "Attempted to log in as {0}"
@ -433,6 +442,10 @@ msgstr "Audience"
#~ msgid "Auth Type"
#~ msgstr "Auth Type"
#: src/flows/sources/apple/AppleLoginInit.ts
msgid "Authenticating with Apple..."
msgstr "Authenticating with Apple..."
#: src/flows/sources/plex/PlexLoginInit.ts
msgid "Authenticating with Plex..."
msgstr "Authenticating with Plex..."
@ -445,6 +458,10 @@ msgstr "Authentication"
msgid "Authentication Type"
msgstr "Authentication Type"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Authentication URL"
msgstr "Authentication URL"
#: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts
@ -760,6 +777,10 @@ msgstr "Check status"
msgid "Check the IP of the Kubernetes service, or"
msgstr "Check the IP of the Kubernetes service, or"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Check the logs"
msgstr "Check the logs"
#:
#~ msgid "Check your Emails for a password reset link."
#~ msgstr "Check your Emails for a password reset link."
@ -1200,6 +1221,10 @@ msgstr "Create Token"
msgid "Create User"
msgstr "Create User"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Create a new application"
msgstr "Create a new application"
#: src/pages/users/ServiceAccountForm.ts
msgid "Create group"
msgstr "Create group"
@ -1260,6 +1285,10 @@ msgstr "Customisation"
msgid "DSA-SHA1"
msgstr "DSA-SHA1"
#: src/interfaces/AdminInterface.ts
msgid "Dashboards"
msgstr "Dashboards"
#: src/pages/stages/prompt/PromptForm.ts
msgid "Date"
msgstr "Date"
@ -1449,6 +1478,10 @@ msgstr "Digits"
msgid "Direct querying, always returns the latest data, but slower than cached querying."
msgstr "Direct querying, always returns the latest data, but slower than cached querying."
#: src/interfaces/AdminInterface.ts
msgid "Directory"
msgstr "Directory"
#:
#:
#~ msgid "Disable"
@ -1818,6 +1851,10 @@ msgstr "Expiry date"
msgid "Explicit Consent"
msgstr "Explicit Consent"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Explore integrations"
msgstr "Explore integrations"
#: src/pages/flows/FlowViewPage.ts
msgid "Export"
msgstr "Export"
@ -1855,7 +1892,6 @@ msgstr "External Applications which use authentik as Identity-Provider, utilizin
msgid "External Host"
msgstr "External Host"
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "External host"
@ -1866,6 +1902,10 @@ msgstr "External host"
msgid "Failed Logins"
msgstr "Failed Logins"
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Failed Logins per day in the last month"
msgstr "Failed Logins per day in the last month"
#: src/pages/stages/password/PasswordStageForm.ts
msgid "Failed attempts before cancel"
msgstr "Failed attempts before cancel"
@ -1899,6 +1939,11 @@ msgstr "Failed to update {0}: {1}"
msgid "Favicon"
msgstr "Favicon"
#: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts
msgid "Federation & Social login"
msgstr "Federation & Social login"
#: src/pages/stages/prompt/PromptListPage.ts
msgid "Field"
msgstr "Field"
@ -1944,6 +1989,10 @@ msgstr "Flow"
msgid "Flow Overview"
msgstr "Flow Overview"
#: src/pages/events/utils.ts
msgid "Flow execution"
msgstr "Flow execution"
#: src/flows/FlowInspector.ts
#: src/flows/FlowInspector.ts
msgid "Flow inspector"
@ -2270,8 +2319,8 @@ msgid "Identifier"
msgstr "Identifier"
#: src/interfaces/AdminInterface.ts
msgid "Identity & Cryptography"
msgstr "Identity & Cryptography"
#~ msgid "Identity & Cryptography"
#~ msgstr "Identity & Cryptography"
#: src/pages/outposts/ServiceConnectionDockerForm.ts
#: src/pages/outposts/ServiceConnectionKubernetesForm.ts
@ -2344,6 +2393,10 @@ msgstr "Import certificates of external providers or create certificates to sign
msgid "In case you can't access any other method."
msgstr "In case you can't access any other method."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com."
msgstr "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com."
#: src/pages/users/UserListPage.ts
msgid "Inactive"
msgstr "Inactive"
@ -2366,8 +2419,8 @@ msgid "Integration key"
msgstr "Integration key"
#: src/interfaces/AdminInterface.ts
msgid "Integrations"
msgstr "Integrations"
#~ msgid "Integrations"
#~ msgstr "Integrations"
#: src/pages/tokens/TokenForm.ts
#: src/pages/tokens/TokenListPage.ts
@ -2689,6 +2742,10 @@ msgstr "Logins"
msgid "Logins over the last 24 hours"
msgstr "Logins over the last 24 hours"
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Logins per day in the last month"
msgstr "Logins per day in the last month"
#: src/pages/tenants/TenantForm.ts
msgid "Logo"
msgstr "Logo"
@ -2996,6 +3053,10 @@ msgstr "No Stages bound"
msgid "No additional data available."
msgstr "No additional data available."
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "No additional setup is required."
msgstr "No additional setup is required."
#: src/elements/forms/ModalForm.ts
msgid "No form found"
msgstr "No form found"
@ -3143,6 +3204,10 @@ msgstr "Object field"
msgid "Object uniqueness field"
msgstr "Object uniqueness field"
#: src/elements/charts/AdminModelPerDay.ts
msgid "Objects created"
msgstr "Objects created"
#: src/pages/stages/consent/ConsentStageForm.ts
msgid "Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3)."
msgstr "Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3)."
@ -3161,6 +3226,10 @@ msgstr "Only fail the policy, don't invalidate user's password."
msgid "Only send notification once, for example when sending a webhook into a chat channel."
msgstr "Only send notification once, for example when sending a webhook into a chat channel."
#: src/elements/notifications/APIDrawer.ts
msgid "Open API Browser"
msgstr "Open API Browser"
#:
#~ msgid "Open application"
#~ msgstr "Open application"
@ -3210,8 +3279,8 @@ msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
msgstr "Optionally set the 'FriendlyName' value of the Assertion attribute."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#~ msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#~ msgstr "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
@ -3239,6 +3308,10 @@ msgstr "Outdated outposts"
msgid "Outpost Deployment Info"
msgstr "Outpost Deployment Info"
#: src/interfaces/AdminInterface.ts
msgid "Outpost Integrations"
msgstr "Outpost Integrations"
#:
#~ msgid "Outpost Service-connection"
#~ msgstr "Outpost Service-connection"
@ -3259,7 +3332,6 @@ msgstr "Outpost status"
msgid "Outpost(s)"
msgstr "Outpost(s)"
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/outposts/OutpostListPage.ts
msgid "Outposts"
@ -3365,7 +3437,6 @@ msgid "Please enter your password"
msgstr "Please enter your password"
#: src/interfaces/AdminInterface.ts
#: src/pages/admin-overview/AdminOverviewPage.ts
#: src/pages/flows/FlowListPage.ts
#: src/pages/policies/PolicyListPage.ts
msgid "Policies"
@ -3584,6 +3655,10 @@ msgstr "Public key, acquired from https://www.google.com/recaptcha/intro/v3.html
msgid "Publisher"
msgstr "Publisher"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Quick actions"
msgstr "Quick actions"
#: src/pages/flows/StageBindingForm.ts
msgid "RESTART restarts the flow from the beginning, while keeping the flow context."
msgstr "RESTART restarts the flow from the beginning, while keeping the flow context."
@ -3761,14 +3836,18 @@ msgid "Reset Password"
msgstr "Reset Password"
#: src/interfaces/AdminInterface.ts
msgid "Resources"
msgstr "Resources"
#~ msgid "Resources"
#~ msgstr "Resources"
#: src/pages/events/EventInfo.ts
#: src/pages/property-mappings/PropertyMappingTestForm.ts
msgid "Result"
msgstr "Result"
#: src/flows/sources/apple/AppleLoginInit.ts
msgid "Retry"
msgstr "Retry"
#:
#~ msgid "Retry Task"
#~ msgstr "Retry Task"
@ -4109,6 +4188,10 @@ msgstr "Set a custom HTTP-Basic Authentication header based on values from authe
msgid "Set custom attributes using YAML or JSON."
msgstr "Set custom attributes using YAML or JSON."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr "Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "Setup"
msgstr "Setup"
@ -4206,8 +4289,6 @@ msgstr "Source {0}"
msgid "Source(s)"
msgstr "Source(s)"
#: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Sources"
msgstr "Sources"
@ -4298,6 +4379,7 @@ msgstr "Stage(s)"
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
#: src/pages/stages/captcha/CaptchaStageForm.ts
#: src/pages/stages/consent/ConsentStageForm.ts
#: src/pages/stages/email/EmailStageForm.ts
@ -4734,9 +4816,13 @@ msgstr "Sync status"
msgid "Sync users"
msgstr "Sync users"
#: src/interfaces/AdminInterface.ts
msgid "System"
msgstr "System"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "System Overview"
msgstr "System Overview"
#~ msgid "System Overview"
#~ msgstr "System Overview"
#: src/interfaces/AdminInterface.ts
#: src/pages/system-tasks/SystemTaskListPage.ts
@ -4845,8 +4931,12 @@ msgid "The external URL you'll access the application at. Include any non-standa
msgstr "The external URL you'll access the application at. Include any non-standard port."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
msgstr "The external URL you'll authenticate at. Can be the same domain as authentik."
#~ msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
#~ msgstr "The external URL you'll authenticate at. Can be the same domain as authentik."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. The authentik core server should be reachable under this URL."
msgstr "The external URL you'll authenticate at. The authentik core server should be reachable under this URL."
#:
#~ msgid "The following objects use {0}:"
@ -5115,6 +5205,7 @@ msgid "UI settings"
msgstr "UI settings"
#: src/pages/events/EventInfo.ts
#: src/pages/users/UserListPage.ts
msgid "UID"
msgstr "UID"
@ -5467,6 +5558,10 @@ msgstr "User interface"
msgid "User matching mode"
msgstr "User matching mode"
#: src/pages/admin-overview/UserDashboardPage.ts
#~ msgid "User metrics"
#~ msgstr "User metrics"
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "User object filter"
msgstr "User object filter"
@ -5475,10 +5570,30 @@ msgstr "User object filter"
msgid "User password writeback"
msgstr "User password writeback"
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "User statistics"
msgstr "User statistics"
#: src/pages/users/UserListPage.ts
msgid "User status"
msgstr "User status"
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification"
msgstr "User verification"
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification is preferred if available, but not required."
msgstr "User verification is preferred if available, but not required."
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification must occur."
msgstr "User verification must occur."
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification should not occur."
msgstr "User verification should not occur."
#: src/pages/events/utils.ts
msgid "User was written to"
msgstr "User was written to"
@ -5529,6 +5644,7 @@ msgstr "Username"
msgid "Username: Same as Text input, but checks for and prevents duplicate usernames."
msgstr "Username: Same as Text input, but checks for and prevents duplicate usernames."
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/admin-overview/AdminOverviewPage.ts
#: src/pages/users/UserListPage.ts
@ -5539,6 +5655,10 @@ msgstr "Users"
msgid "Users added to this group will be superusers."
msgstr "Users added to this group will be superusers."
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Users created per day in the last month"
msgstr "Users created per day in the last month"
#: src/pages/providers/ldap/LDAPProviderForm.ts
msgid "Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed."
msgstr "Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed."
@ -5651,6 +5771,10 @@ msgstr "Warning: Provider is not used by any Outpost."
msgid "Warning: Provider not assigned to any application."
msgstr "Warning: Provider not assigned to any application."
#: src/pages/users/UserListPage.ts
msgid "Warning: You're about to delete the user you're logged in as ({0}). Proceed at your own risk."
msgstr "Warning: You're about to delete the user you're logged in as ({0}). Proceed at your own risk."
#: src/pages/outposts/OutpostListPage.ts
msgid "Warning: authentik Domain is not configured, authentication will not work."
msgstr "Warning: authentik Domain is not configured, authentication will not work."
@ -5679,6 +5803,10 @@ msgstr "Webhook Mapping"
msgid "Webhook URL"
msgstr "Webhook URL"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Welcome, {name}."
msgstr "Welcome, {name}."
#: src/pages/stages/email/EmailStageForm.ts
msgid "When a user returns from the email successfully, their account will be activated."
msgstr "When a user returns from the email successfully, their account will be activated."
@ -5782,6 +5910,10 @@ msgstr "You're about to be redirect to the following URL."
msgid "You're currently impersonating {0}. Click to stop."
msgstr "You're currently impersonating {0}. Click to stop."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "app1 running on app1.example.com"
msgstr "app1 running on app1.example.com"
#:
#~ msgid "authentik Builtin Database"
#~ msgstr "authentik Builtin Database"
@ -5790,6 +5922,10 @@ msgstr "You're currently impersonating {0}. Click to stop."
#~ msgid "authentik LDAP Backend"
#~ msgstr "authentik LDAP Backend"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "authentik running on auth.example.com"
msgstr "authentik running on auth.example.com"
#: src/elements/forms/DeleteForm.ts
msgid "connecting object will be deleted"
msgstr "connecting object will be deleted"
@ -5858,3 +5994,11 @@ msgstr "{0}, should be {1}"
#: src/elements/forms/ConfirmationForm.ts
msgid "{0}: {1}"
msgstr "{0}: {1}"
#: src/elements/charts/AdminModelPerDay.ts
msgid "{ago} days ago"
msgstr "{ago} days ago"
#: src/elements/charts/Chart.ts
msgid "{ago} hours ago"
msgstr "{ago} hours ago"

View file

@ -300,6 +300,10 @@ msgstr "Sinon, si Duo est installé sur cet appareil, cliquez sur ce lien :"
msgid "Always require consent"
msgstr "Toujours exiger l'approbation"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "An example setup can look like this:"
msgstr ""
#: src/pages/stages/prompt/PromptForm.ts
msgid "Any HTML can be used."
msgstr ""
@ -348,6 +352,7 @@ msgstr "Nom d'affichage de l'application"
msgid "Application(s)"
msgstr "Application(s)"
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/applications/ApplicationListPage.ts
#: src/pages/outposts/OutpostForm.ts
@ -411,8 +416,12 @@ msgid "Assigned to application"
msgstr "Assigné à l'application"
#: src/pages/policies/PolicyListPage.ts
msgid "Assigned to {0} objects."
msgstr "Assigné à {0} objets"
msgid "Assigned to {0} object(s)."
msgstr ""
#: src/pages/policies/PolicyListPage.ts
#~ msgid "Assigned to {0} objects."
#~ msgstr "Assigné à {0} objets"
#: src/pages/events/EventInfo.ts
msgid "Attempted to log in as {0}"
@ -437,6 +446,10 @@ msgstr "Audience"
#~ msgid "Auth Type"
#~ msgstr ""
#: src/flows/sources/apple/AppleLoginInit.ts
msgid "Authenticating with Apple..."
msgstr ""
#: src/flows/sources/plex/PlexLoginInit.ts
msgid "Authenticating with Plex..."
msgstr "Authentification avec Plex..."
@ -449,6 +462,10 @@ msgstr "Authentification"
msgid "Authentication Type"
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Authentication URL"
msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts
@ -761,6 +778,10 @@ msgstr "Vérifier le statut"
msgid "Check the IP of the Kubernetes service, or"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Check the logs"
msgstr ""
#:
#~ msgid "Check your Emails for a password reset link."
#~ msgstr "Vérifiez vos courriels pour un lien de récupération de mot de passe."
@ -1198,6 +1219,10 @@ msgstr "Créer un jeton"
msgid "Create User"
msgstr "Créer un utilisateu"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Create a new application"
msgstr ""
#: src/pages/users/ServiceAccountForm.ts
msgid "Create group"
msgstr "Créer un groupe"
@ -1258,6 +1283,10 @@ msgstr "Personalisation"
msgid "DSA-SHA1"
msgstr "DSA-SHA1"
#: src/interfaces/AdminInterface.ts
msgid "Dashboards"
msgstr ""
#: src/pages/stages/prompt/PromptForm.ts
msgid "Date"
msgstr "Date"
@ -1439,6 +1468,10 @@ msgstr "Chiffres"
msgid "Direct querying, always returns the latest data, but slower than cached querying."
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Directory"
msgstr ""
#~ msgid "Disable"
#~ msgstr "Désactiver"
@ -1804,6 +1837,10 @@ msgstr "Date d'expiration"
msgid "Explicit Consent"
msgstr "Approbation explicite"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Explore integrations"
msgstr ""
#: src/pages/flows/FlowViewPage.ts
msgid "Export"
msgstr "Exporter"
@ -1841,7 +1878,6 @@ msgstr "Applications externes qui utilisent authentik comme fournisseur d'identi
msgid "External Host"
msgstr "Hôte externe"
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "External host"
@ -1852,6 +1888,10 @@ msgstr "Hôte externe"
msgid "Failed Logins"
msgstr "Connexions échouées"
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Failed Logins per day in the last month"
msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "Failed attempts before cancel"
msgstr "Échecs avant annulation"
@ -1885,6 +1925,11 @@ msgstr "Impossible de mettre à jour {0} : {1}"
msgid "Favicon"
msgstr "Favicon"
#: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts
msgid "Federation & Social login"
msgstr ""
#: src/pages/stages/prompt/PromptListPage.ts
msgid "Field"
msgstr "Champ"
@ -1929,6 +1974,10 @@ msgstr "Flux"
msgid "Flow Overview"
msgstr "Aperçu du flux"
#: src/pages/events/utils.ts
msgid "Flow execution"
msgstr ""
#: src/flows/FlowInspector.ts
#: src/flows/FlowInspector.ts
msgid "Flow inspector"
@ -2253,8 +2302,8 @@ msgid "Identifier"
msgstr "Identifiant"
#: src/interfaces/AdminInterface.ts
msgid "Identity & Cryptography"
msgstr "Identité et chiffrement"
#~ msgid "Identity & Cryptography"
#~ msgstr "Identité et chiffrement"
#: src/pages/outposts/ServiceConnectionDockerForm.ts
#: src/pages/outposts/ServiceConnectionKubernetesForm.ts
@ -2327,6 +2376,10 @@ msgstr "Importer les certificats des fournisseurs externes ou créer des certifi
msgid "In case you can't access any other method."
msgstr "Au cas où aucune autre méthode ne soit disponible."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com."
msgstr ""
#: src/pages/users/UserListPage.ts
msgid "Inactive"
msgstr "Inactif"
@ -2349,8 +2402,8 @@ msgid "Integration key"
msgstr "Clé d'intégration"
#: src/interfaces/AdminInterface.ts
msgid "Integrations"
msgstr "Intégrations"
#~ msgid "Integrations"
#~ msgstr "Intégrations"
#: src/pages/tokens/TokenForm.ts
#: src/pages/tokens/TokenListPage.ts
@ -2668,6 +2721,10 @@ msgstr "Connexions"
msgid "Logins over the last 24 hours"
msgstr "Connexions ces dernières 24 heures"
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Logins per day in the last month"
msgstr ""
#: src/pages/tenants/TenantForm.ts
msgid "Logo"
msgstr "Logo"
@ -2974,6 +3031,10 @@ msgstr "Aucune étape liée"
msgid "No additional data available."
msgstr "Aucune donnée additionnelle disponible."
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "No additional setup is required."
msgstr ""
#: src/elements/forms/ModalForm.ts
msgid "No form found"
msgstr "Aucun formulaire trouvé"
@ -3119,6 +3180,10 @@ msgstr "Champ d'objet"
msgid "Object uniqueness field"
msgstr "Champ d'unicité de l'objet"
#: src/elements/charts/AdminModelPerDay.ts
msgid "Objects created"
msgstr ""
#: src/pages/stages/consent/ConsentStageForm.ts
msgid "Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3)."
msgstr "Durée d'expiration du consentement (Format : hours=1;minutes=2;seconds=3)."
@ -3137,6 +3202,10 @@ msgstr "Faire simplement échouer la politique sans invalider le mot de passe ut
msgid "Only send notification once, for example when sending a webhook into a chat channel."
msgstr "Envoyer une seule fois la notification, par exemple lors de l'envoi d'un webhook dans un canal de discussion."
#: src/elements/notifications/APIDrawer.ts
msgid "Open API Browser"
msgstr ""
#~ msgid "Open application"
#~ msgstr "Ouvrir l'appication"
@ -3185,8 +3254,8 @@ msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
msgstr "Indiquer la valeur \"FriendlyName\" de l'attribut d'assertion (optionnel)"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr "Indiquer votre domaine parent (optionnel), si vous souhaitez que l'authentification et l'autorisation soient réalisés au niveau du domaine. Si vous exécutez des applications sur app1.domain.tld, app2.domain.tld, indiquez ici \"domain.tld\"."
#~ msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#~ msgstr "Indiquer votre domaine parent (optionnel), si vous souhaitez que l'authentification et l'autorisation soient réalisés au niveau du domaine. Si vous exécutez des applications sur app1.domain.tld, app2.domain.tld, indiquez ici \"domain.tld\"."
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
@ -3213,6 +3282,10 @@ msgstr "Avant-postes périmés"
msgid "Outpost Deployment Info"
msgstr "Info de déploiement de l'avant-poste"
#: src/interfaces/AdminInterface.ts
msgid "Outpost Integrations"
msgstr ""
#~ msgid "Outpost Service-connection"
#~ msgstr "Connexion de service de l'avant-poste"
@ -3231,7 +3304,6 @@ msgstr "Statut de l'avant-poste"
msgid "Outpost(s)"
msgstr "Avant-poste(s)"
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/outposts/OutpostListPage.ts
msgid "Outposts"
@ -3337,7 +3409,6 @@ msgid "Please enter your password"
msgstr "Veuillez saisir votre mot de passe"
#: src/interfaces/AdminInterface.ts
#: src/pages/admin-overview/AdminOverviewPage.ts
#: src/pages/flows/FlowListPage.ts
#: src/pages/policies/PolicyListPage.ts
msgid "Policies"
@ -3552,6 +3623,10 @@ msgstr "Clé publique, obtenue depuis https://www.google.com/recaptcha/intro/v3.
msgid "Publisher"
msgstr "Éditeur"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Quick actions"
msgstr ""
#: src/pages/flows/StageBindingForm.ts
msgid "RESTART restarts the flow from the beginning, while keeping the flow context."
msgstr "REDÉMARRER redémarre le flux depuis le début, en gardant le contexte du flux."
@ -3732,14 +3807,18 @@ msgid "Reset Password"
msgstr "Réinitialiser le mot de passe"
#: src/interfaces/AdminInterface.ts
msgid "Resources"
msgstr "Ressources"
#~ msgid "Resources"
#~ msgstr "Ressources"
#: src/pages/events/EventInfo.ts
#: src/pages/property-mappings/PropertyMappingTestForm.ts
msgid "Result"
msgstr "Résultat"
#: src/flows/sources/apple/AppleLoginInit.ts
msgid "Retry"
msgstr ""
#~ msgid "Retry Task"
#~ msgstr "Réessayer la tâche"
@ -4072,6 +4151,10 @@ msgstr "Définir un en-tête d'authentification HTTP-Basic personnalisé basé s
msgid "Set custom attributes using YAML or JSON."
msgstr "Définissez des attributs personnalisés via YAML ou JSON."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "Setup"
msgstr ""
@ -4168,8 +4251,6 @@ msgstr "Source {0}"
msgid "Source(s)"
msgstr "Source(s)"
#: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Sources"
msgstr "Sources"
@ -4258,6 +4339,7 @@ msgstr "Étape(s)"
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
#: src/pages/stages/captcha/CaptchaStageForm.ts
#: src/pages/stages/consent/ConsentStageForm.ts
#: src/pages/stages/email/EmailStageForm.ts
@ -4689,9 +4771,13 @@ msgstr "Synchroniser les statuts"
msgid "Sync users"
msgstr "Synchroniser les utilisateurs"
#: src/interfaces/AdminInterface.ts
msgid "System"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "System Overview"
msgstr "Vue d'ensemble du système"
#~ msgid "System Overview"
#~ msgstr "Vue d'ensemble du système"
#: src/interfaces/AdminInterface.ts
#: src/pages/system-tasks/SystemTaskListPage.ts
@ -4799,8 +4885,12 @@ msgid "The external URL you'll access the application at. Include any non-standa
msgstr "L'URL externe par laquelle vous accéderez à l'application. Incluez un port non-standard si besoin."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
msgstr "L'URL externe sur laquelle vous vous authentifierez. Cela peut être le même domaine qu'authentik."
#~ msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
#~ msgstr "L'URL externe sur laquelle vous vous authentifierez. Cela peut être le même domaine qu'authentik."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. The authentik core server should be reachable under this URL."
msgstr ""
#~ msgid "The following objects use {0}:"
#~ msgstr "Les objets suivants utilisent {0} :"
@ -5056,6 +5146,7 @@ msgid "UI settings"
msgstr "Paramètres d'UI"
#: src/pages/events/EventInfo.ts
#: src/pages/users/UserListPage.ts
msgid "UID"
msgstr "UID"
@ -5405,6 +5496,10 @@ msgstr "Interface utilisateur"
msgid "User matching mode"
msgstr "Mode de correspondance utilisateur"
#: src/pages/admin-overview/UserDashboardPage.ts
#~ msgid "User metrics"
#~ msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "User object filter"
msgstr "Filtre des objets utilisateur"
@ -5413,10 +5508,30 @@ msgstr "Filtre des objets utilisateur"
msgid "User password writeback"
msgstr "Réécriture du mot de passe utilisateur"
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "User statistics"
msgstr ""
#: src/pages/users/UserListPage.ts
msgid "User status"
msgstr "Statut utilisateur"
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification"
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification is preferred if available, but not required."
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification must occur."
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification should not occur."
msgstr ""
#: src/pages/events/utils.ts
msgid "User was written to"
msgstr "L'utilisateur a été écrit vers "
@ -5467,6 +5582,7 @@ msgstr "Nom d'utilisateur"
msgid "Username: Same as Text input, but checks for and prevents duplicate usernames."
msgstr "Nom d'utilisateur : Identique à la saisie de texte, mais vérifie et empêche les noms d'utilisateur en double."
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/admin-overview/AdminOverviewPage.ts
#: src/pages/users/UserListPage.ts
@ -5477,6 +5593,10 @@ msgstr "Utilisateurs"
msgid "Users added to this group will be superusers."
msgstr "Les utilisateurs ajoutés à ce groupe seront des super-utilisateurs."
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Users created per day in the last month"
msgstr ""
#: src/pages/providers/ldap/LDAPProviderForm.ts
msgid "Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed."
msgstr "Les utilisateurs de ce groupe peuvent effectuer des recherches. Si aucun groupe n'est sélectionné, aucune recherche LDAP n'est autorisée."
@ -5589,6 +5709,10 @@ msgstr ""
msgid "Warning: Provider not assigned to any application."
msgstr "Avertissement : le fournisseur n'est assigné à aucune application."
#: src/pages/users/UserListPage.ts
msgid "Warning: You're about to delete the user you're logged in as ({0}). Proceed at your own risk."
msgstr ""
#: src/pages/outposts/OutpostListPage.ts
msgid "Warning: authentik Domain is not configured, authentication will not work."
msgstr "Avertissement : le domaine d'authentik n'est pas configuré, l'authentification ne fonctionnera pas."
@ -5617,6 +5741,10 @@ msgstr "Mapping Webhook"
msgid "Webhook URL"
msgstr "URL Webhoo"
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Welcome, {name}."
msgstr ""
#: src/pages/stages/email/EmailStageForm.ts
msgid "When a user returns from the email successfully, their account will be activated."
msgstr "Lorsqu'un utilisateur revient de l'e-mail avec succès, son compte sera activé."
@ -5718,12 +5846,20 @@ msgstr "Vous allez être redirigé vers l'URL suivante."
msgid "You're currently impersonating {0}. Click to stop."
msgstr "Vous êtes en train de vous faire passer pour {0}. Cliquez pour arrêter."
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "app1 running on app1.example.com"
msgstr ""
#~ msgid "authentik Builtin Database"
#~ msgstr "Base de données intégrée à authentik"
#~ msgid "authentik LDAP Backend"
#~ msgstr "Backend LDAP authentik"
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "authentik running on auth.example.com"
msgstr ""
#: src/elements/forms/DeleteForm.ts
msgid "connecting object will be deleted"
msgstr "L'objet connecté sera supprimé"
@ -5792,3 +5928,11 @@ msgstr "{0}, devrait être {1}"
#: src/elements/forms/ConfirmationForm.ts
msgid "{0}: {1}"
msgstr "{0} : {1}"
#: src/elements/charts/AdminModelPerDay.ts
msgid "{ago} days ago"
msgstr ""
#: src/elements/charts/Chart.ts
msgid "{ago} hours ago"
msgstr ""

View file

@ -296,6 +296,10 @@ msgstr ""
msgid "Always require consent"
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "An example setup can look like this:"
msgstr ""
#: src/pages/stages/prompt/PromptForm.ts
msgid "Any HTML can be used."
msgstr ""
@ -344,6 +348,7 @@ msgstr ""
msgid "Application(s)"
msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/applications/ApplicationListPage.ts
#: src/pages/outposts/OutpostForm.ts
@ -403,9 +408,13 @@ msgid "Assigned to application"
msgstr ""
#: src/pages/policies/PolicyListPage.ts
msgid "Assigned to {0} objects."
msgid "Assigned to {0} object(s)."
msgstr ""
#: src/pages/policies/PolicyListPage.ts
#~ msgid "Assigned to {0} objects."
#~ msgstr ""
#: src/pages/events/EventInfo.ts
msgid "Attempted to log in as {0}"
msgstr ""
@ -429,6 +438,10 @@ msgstr ""
#~ msgid "Auth Type"
#~ msgstr ""
#: src/flows/sources/apple/AppleLoginInit.ts
msgid "Authenticating with Apple..."
msgstr ""
#: src/flows/sources/plex/PlexLoginInit.ts
msgid "Authenticating with Plex..."
msgstr ""
@ -441,6 +454,10 @@ msgstr ""
msgid "Authentication Type"
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Authentication URL"
msgstr ""
#: src/pages/sources/oauth/OAuthSourceForm.ts
#: src/pages/sources/plex/PlexSourceForm.ts
#: src/pages/sources/saml/SAMLSourceForm.ts
@ -756,6 +773,10 @@ msgstr ""
msgid "Check the IP of the Kubernetes service, or"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Check the logs"
msgstr ""
#:
#~ msgid "Check your Emails for a password reset link."
#~ msgstr ""
@ -1194,6 +1215,10 @@ msgstr ""
msgid "Create User"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Create a new application"
msgstr ""
#: src/pages/users/ServiceAccountForm.ts
msgid "Create group"
msgstr ""
@ -1254,6 +1279,10 @@ msgstr ""
msgid "DSA-SHA1"
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Dashboards"
msgstr ""
#: src/pages/stages/prompt/PromptForm.ts
msgid "Date"
msgstr ""
@ -1441,6 +1470,10 @@ msgstr ""
msgid "Direct querying, always returns the latest data, but slower than cached querying."
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Directory"
msgstr ""
#:
#:
#~ msgid "Disable"
@ -1810,6 +1843,10 @@ msgstr ""
msgid "Explicit Consent"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Explore integrations"
msgstr ""
#: src/pages/flows/FlowViewPage.ts
msgid "Export"
msgstr ""
@ -1847,7 +1884,6 @@ msgstr ""
msgid "External Host"
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "External host"
@ -1858,6 +1894,10 @@ msgstr ""
msgid "Failed Logins"
msgstr ""
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Failed Logins per day in the last month"
msgstr ""
#: src/pages/stages/password/PasswordStageForm.ts
msgid "Failed attempts before cancel"
msgstr ""
@ -1891,6 +1931,11 @@ msgstr ""
msgid "Favicon"
msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts
msgid "Federation & Social login"
msgstr ""
#: src/pages/stages/prompt/PromptListPage.ts
msgid "Field"
msgstr ""
@ -1936,6 +1981,10 @@ msgstr ""
msgid "Flow Overview"
msgstr ""
#: src/pages/events/utils.ts
msgid "Flow execution"
msgstr ""
#: src/flows/FlowInspector.ts
#: src/flows/FlowInspector.ts
msgid "Flow inspector"
@ -2262,8 +2311,8 @@ msgid "Identifier"
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Identity & Cryptography"
msgstr ""
#~ msgid "Identity & Cryptography"
#~ msgstr ""
#: src/pages/outposts/ServiceConnectionDockerForm.ts
#: src/pages/outposts/ServiceConnectionKubernetesForm.ts
@ -2336,6 +2385,10 @@ msgstr ""
msgid "In case you can't access any other method."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "In this case, you'd set the Authentication URL to auth.example.com and Cookie domain to example.com."
msgstr ""
#: src/pages/users/UserListPage.ts
msgid "Inactive"
msgstr ""
@ -2358,8 +2411,8 @@ msgid "Integration key"
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Integrations"
msgstr ""
#~ msgid "Integrations"
#~ msgstr ""
#: src/pages/tokens/TokenForm.ts
#: src/pages/tokens/TokenListPage.ts
@ -2679,6 +2732,10 @@ msgstr ""
msgid "Logins over the last 24 hours"
msgstr ""
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Logins per day in the last month"
msgstr ""
#: src/pages/tenants/TenantForm.ts
msgid "Logo"
msgstr ""
@ -2986,6 +3043,10 @@ msgstr ""
msgid "No additional data available."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "No additional setup is required."
msgstr ""
#: src/elements/forms/ModalForm.ts
msgid "No form found"
msgstr ""
@ -3133,6 +3194,10 @@ msgstr ""
msgid "Object uniqueness field"
msgstr ""
#: src/elements/charts/AdminModelPerDay.ts
msgid "Objects created"
msgstr ""
#: src/pages/stages/consent/ConsentStageForm.ts
msgid "Offset after which consent expires. (Format: hours=1;minutes=2;seconds=3)."
msgstr ""
@ -3151,6 +3216,10 @@ msgstr ""
msgid "Only send notification once, for example when sending a webhook into a chat channel."
msgstr ""
#: src/elements/notifications/APIDrawer.ts
msgid "Open API Browser"
msgstr ""
#:
#~ msgid "Open application"
#~ msgstr ""
@ -3200,8 +3269,8 @@ msgid "Optionally set the 'FriendlyName' value of the Assertion attribute."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr ""
#~ msgid "Optionally set this to your parent domain, if you want authentication and authorization to happen on a domain level. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
#~ msgstr ""
#: src/pages/flows/BoundStagesList.ts
#: src/pages/flows/StageBindingForm.ts
@ -3229,6 +3298,10 @@ msgstr ""
msgid "Outpost Deployment Info"
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Outpost Integrations"
msgstr ""
#:
#~ msgid "Outpost Service-connection"
#~ msgstr ""
@ -3249,7 +3322,6 @@ msgstr ""
msgid "Outpost(s)"
msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/outposts/OutpostListPage.ts
msgid "Outposts"
@ -3355,7 +3427,6 @@ msgid "Please enter your password"
msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/pages/admin-overview/AdminOverviewPage.ts
#: src/pages/flows/FlowListPage.ts
#: src/pages/policies/PolicyListPage.ts
msgid "Policies"
@ -3574,6 +3645,10 @@ msgstr ""
msgid "Publisher"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Quick actions"
msgstr ""
#: src/pages/flows/StageBindingForm.ts
msgid "RESTART restarts the flow from the beginning, while keeping the flow context."
msgstr ""
@ -3751,14 +3826,18 @@ msgid "Reset Password"
msgstr ""
#: src/interfaces/AdminInterface.ts
msgid "Resources"
msgstr ""
#~ msgid "Resources"
#~ msgstr ""
#: src/pages/events/EventInfo.ts
#: src/pages/property-mappings/PropertyMappingTestForm.ts
msgid "Result"
msgstr ""
#: src/flows/sources/apple/AppleLoginInit.ts
msgid "Retry"
msgstr ""
#:
#~ msgid "Retry Task"
#~ msgstr ""
@ -4099,6 +4178,10 @@ msgstr ""
msgid "Set custom attributes using YAML or JSON."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "Set this to the domain you wish the authentication to be valid for. Must be a parent domain of the URL above. If you're running applications as app1.domain.tld, app2.domain.tld, set this to 'domain.tld'."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderViewPage.ts
msgid "Setup"
msgstr ""
@ -4196,8 +4279,6 @@ msgstr ""
msgid "Source(s)"
msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/pages/sources/SourcesListPage.ts
#: src/pages/stages/identification/IdentificationStageForm.ts
msgid "Sources"
msgstr ""
@ -4288,6 +4369,7 @@ msgstr ""
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
#: src/pages/stages/authenticator_validate/AuthenticatorValidateStageForm.ts
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
#: src/pages/stages/captcha/CaptchaStageForm.ts
#: src/pages/stages/consent/ConsentStageForm.ts
#: src/pages/stages/email/EmailStageForm.ts
@ -4724,10 +4806,14 @@ msgstr ""
msgid "Sync users"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "System Overview"
#: src/interfaces/AdminInterface.ts
msgid "System"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
#~ msgid "System Overview"
#~ msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/pages/system-tasks/SystemTaskListPage.ts
msgid "System Tasks"
@ -4835,7 +4921,11 @@ msgid "The external URL you'll access the application at. Include any non-standa
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
#~ msgid "The external URL you'll authenticate at. Can be the same domain as authentik."
#~ msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "The external URL you'll authenticate at. The authentik core server should be reachable under this URL."
msgstr ""
#:
@ -5095,6 +5185,7 @@ msgid "UI settings"
msgstr ""
#: src/pages/events/EventInfo.ts
#: src/pages/users/UserListPage.ts
msgid "UID"
msgstr ""
@ -5447,6 +5538,10 @@ msgstr ""
msgid "User matching mode"
msgstr ""
#: src/pages/admin-overview/UserDashboardPage.ts
#~ msgid "User metrics"
#~ msgstr ""
#: src/pages/sources/ldap/LDAPSourceForm.ts
msgid "User object filter"
msgstr ""
@ -5455,10 +5550,30 @@ msgstr ""
msgid "User password writeback"
msgstr ""
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "User statistics"
msgstr ""
#: src/pages/users/UserListPage.ts
msgid "User status"
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification"
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification is preferred if available, but not required."
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification must occur."
msgstr ""
#: src/pages/stages/authenticator_webauthn/AuthenticateWebAuthnStageForm.ts
msgid "User verification should not occur."
msgstr ""
#: src/pages/events/utils.ts
msgid "User was written to"
msgstr ""
@ -5509,6 +5624,7 @@ msgstr ""
msgid "Username: Same as Text input, but checks for and prevents duplicate usernames."
msgstr ""
#: src/interfaces/AdminInterface.ts
#: src/interfaces/AdminInterface.ts
#: src/pages/admin-overview/AdminOverviewPage.ts
#: src/pages/users/UserListPage.ts
@ -5519,6 +5635,10 @@ msgstr ""
msgid "Users added to this group will be superusers."
msgstr ""
#: src/pages/admin-overview/DashboardUserPage.ts
msgid "Users created per day in the last month"
msgstr ""
#: src/pages/providers/ldap/LDAPProviderForm.ts
msgid "Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed."
msgstr ""
@ -5631,6 +5751,10 @@ msgstr ""
msgid "Warning: Provider not assigned to any application."
msgstr ""
#: src/pages/users/UserListPage.ts
msgid "Warning: You're about to delete the user you're logged in as ({0}). Proceed at your own risk."
msgstr ""
#: src/pages/outposts/OutpostListPage.ts
msgid "Warning: authentik Domain is not configured, authentication will not work."
msgstr ""
@ -5659,6 +5783,10 @@ msgstr ""
msgid "Webhook URL"
msgstr ""
#: src/pages/admin-overview/AdminOverviewPage.ts
msgid "Welcome, {name}."
msgstr ""
#: src/pages/stages/email/EmailStageForm.ts
msgid "When a user returns from the email successfully, their account will be activated."
msgstr ""
@ -5760,6 +5888,10 @@ msgstr ""
msgid "You're currently impersonating {0}. Click to stop."
msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "app1 running on app1.example.com"
msgstr ""
#:
#~ msgid "authentik Builtin Database"
#~ msgstr ""
@ -5768,6 +5900,10 @@ msgstr ""
#~ msgid "authentik LDAP Backend"
#~ msgstr ""
#: src/pages/providers/proxy/ProxyProviderForm.ts
msgid "authentik running on auth.example.com"
msgstr ""
#: src/elements/forms/DeleteForm.ts
msgid "connecting object will be deleted"
msgstr ""
@ -5836,3 +5972,11 @@ msgstr ""
#: src/elements/forms/ConfirmationForm.ts
msgid "{0}: {1}"
msgstr ""
#: src/elements/charts/AdminModelPerDay.ts
msgid "{ago} days ago"
msgstr ""
#: src/elements/charts/Chart.ts
msgid "{ago} hours ago"
msgstr ""

View file

@ -2,15 +2,19 @@ import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import { until } from "lit/directives/until.js";
import AKGlobal from "../../authentik.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import { me } from "../../api/Users";
import "../../elements/PageHeader";
import "../../elements/cards/AggregatePromiseCard";
import "../../elements/charts/AdminLoginsChart";
import { paramURL } from "../../elements/router/RouterOutlet";
import "./TopApplicationsTable";
import "./cards/AdminStatusCard";
import "./cards/BackupStatusCard";
@ -31,6 +35,7 @@ export class AdminOverviewPage extends LitElement {
PFGrid,
PFPage,
PFContent,
PFList,
AKGlobal,
css`
.row-divider {
@ -51,11 +56,18 @@ export class AdminOverviewPage extends LitElement {
}
render(): TemplateResult {
return html` <ak-page-header
icon=""
header=${t`System Overview`}
description=${t`General system status`}
>
return html`<ak-page-header icon="" header="" description=${t`General system status`}>
<span slot="header">
${until(
me().then((user) => {
let name = user.user.username;
if (user.user.name !== "") {
name = user.user.name;
}
return t`Welcome, ${name}.`;
}),
)}
</span>
</ak-page-header>
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">
@ -64,11 +76,33 @@ export class AdminOverviewPage extends LitElement {
class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-xl pf-m-2-col-on-2xl graph-container"
>
<ak-aggregate-card
icon="pf-icon pf-icon-infrastructure"
header=${t`Policies`}
headerLink="#/policy/policies"
icon="fa fa-share"
header=${t`Quick actions`}
.isCenter=${false}
>
<ak-admin-status-chart-policy></ak-admin-status-chart-policy>
<ul class="pf-c-list">
<li>
<a
class="pf-u-mb-xl"
href=${paramURL("/core/applications", {
createForm: true,
})}
>${t`Create a new application`}</a
>
</li>
<li>
<a class="pf-u-mb-xl" href=${paramURL("/events/log")}
>${t`Check the logs`}</a
>
</li>
<li>
<a
class="pf-u-mb-xl"
href="https://goauthentik.io/integrations/"
>${t`Explore integrations`}</a
>
</li>
</ul>
</ak-aggregate-card>
</div>
<div

View file

@ -0,0 +1,86 @@
import { t } from "@lingui/macro";
import { CSSResult, LitElement, TemplateResult, css, html } from "lit";
import { customElement } from "lit/decorators.js";
import AKGlobal from "../../authentik.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFList from "@patternfly/patternfly/components/List/list.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
import { EventActions } from "@goauthentik/api";
import "../../elements/PageHeader";
import "../../elements/cards/AggregatePromiseCard";
import "../../elements/charts/AdminModelPerDay";
@customElement("ak-admin-dashboard-users")
export class DashboardUserPage extends LitElement {
static get styles(): CSSResult[] {
return [
PFGrid,
PFPage,
PFContent,
PFList,
AKGlobal,
css`
.row-divider {
margin-top: -4px;
margin-bottom: -4px;
}
.graph-container {
height: 20em;
}
.big-graph-container {
height: 35em;
}
.card-container {
max-height: 10em;
}
`,
];
}
render(): TemplateResult {
return html`<ak-page-header icon="pf-icon pf-icon-user" header=${t`User statistics`}>
</ak-page-header>
<section class="pf-c-page__main-section">
<div class="pf-l-grid pf-m-gutter">
<div
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl big-graph-container"
>
<ak-aggregate-card header=${t`Users created per day in the last month`}>
<ak-charts-admin-model-per-day
.query=${{
context__model__app: "authentik_core",
context__model__model_name: "user",
}}
>
</ak-charts-admin-model-per-day>
</ak-aggregate-card>
</div>
<div class="pf-l-grid__item pf-m-12-col row-divider">
<hr />
</div>
<!-- row 2 -->
<div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl big-graph-container"
>
<ak-aggregate-card header=${t`Logins per day in the last month`}>
<ak-charts-admin-model-per-day action=${EventActions.Login}>
</ak-charts-admin-model-per-day>
</ak-aggregate-card>
</div>
<div
class="pf-l-grid__item pf-m-12-col pf-m-6-col-on-xl pf-m-6-col-on-2xl big-graph-container"
>
<ak-aggregate-card header=${t`Failed Logins per day in the last month`}>
<ak-charts-admin-model-per-day action=${EventActions.LoginFailed}>
</ak-charts-admin-model-per-day>
</ak-aggregate-card>
</div>
</div>
</section> `;
}
}

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