diff --git a/.bumpversion.cfg b/.bumpversion.cfg index cbbf767e6..49bf7d956 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -36,3 +36,7 @@ values = [bumpversion:file:outpost/pkg/version.go] [bumpversion:file:web/src/constants.ts] + +[bumpversion:file:website/docs/outpusts/manual-deploy-docker-compose.md] + +[bumpversion:file:website/docs/outpusts/manual-deploy-kubernetes.md] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8d2f320ac..05561850f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 + - name: prepare ts api client + run: | + docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true - name: Docker Login Registry env: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/Dockerfile b/Dockerfile index 5041486ca..a806fb659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,4 +45,5 @@ COPY ./lifecycle/ /lifecycle USER authentik STOPSIGNAL SIGINT ENV TMPDIR /dev/shm/ +ENV PYTHONUBUFFERED 1 ENTRYPOINT [ "/lifecycle/bootstrap.sh" ] diff --git a/Pipfile.lock b/Pipfile.lock index 2431413bf..c57c0f093 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -18,45 +18,45 @@ "default": { "aiohttp": { "hashes": [ - "sha256:119feb2bd551e58d83d1b38bfa4cb921af8ddedec9fad7183132db334c3133e0", - "sha256:16d0683ef8a6d803207f02b899c928223eb219111bd52420ef3d7a8aa76227b6", - "sha256:2eb3efe243e0f4ecbb654b08444ae6ffab37ac0ef8f69d3a2ffb958905379daf", - "sha256:2ffea7904e70350da429568113ae422c88d2234ae776519549513c8f217f58a9", - "sha256:40bd1b101b71a18a528ffce812cc14ff77d4a2a1272dfb8b11b200967489ef3e", - "sha256:418597633b5cd9639e514b1d748f358832c08cd5d9ef0870026535bd5eaefdd0", - "sha256:481d4b96969fbfdcc3ff35eea5305d8565a8300410d3d269ccac69e7256b1329", - "sha256:4c1bdbfdd231a20eee3e56bd0ac1cd88c4ff41b64ab679ed65b75c9c74b6c5c2", - "sha256:5563ad7fde451b1986d42b9bb9140e2599ecf4f8e42241f6da0d3d624b776f40", - "sha256:58c62152c4c8731a3152e7e650b29ace18304d086cb5552d317a54ff2749d32a", - "sha256:5b50e0b9460100fe05d7472264d1975f21ac007b35dcd6fd50279b72925a27f4", - "sha256:5d84ecc73141d0a0d61ece0742bb7ff5751b0657dab8405f899d3ceb104cc7de", - "sha256:5dde6d24bacac480be03f4f864e9a67faac5032e28841b00533cd168ab39cad9", - "sha256:5e91e927003d1ed9283dee9abcb989334fc8e72cf89ebe94dc3e07e3ff0b11e9", - "sha256:62bc216eafac3204877241569209d9ba6226185aa6d561c19159f2e1cbb6abfb", - "sha256:6c8200abc9dc5f27203986100579fc19ccad7a832c07d2bc151ce4ff17190076", - "sha256:6ca56bdfaf825f4439e9e3673775e1032d8b6ea63b8953d3812c71bd6a8b81de", - "sha256:71680321a8a7176a58dfbc230789790639db78dad61a6e120b39f314f43f1907", - "sha256:7c7820099e8b3171e54e7eedc33e9450afe7cd08172632d32128bd527f8cb77d", - "sha256:7dbd087ff2f4046b9b37ba28ed73f15fd0bc9f4fdc8ef6781913da7f808d9536", - "sha256:822bd4fd21abaa7b28d65fc9871ecabaddc42767884a626317ef5b75c20e8a2d", - "sha256:8ec1a38074f68d66ccb467ed9a673a726bb397142c273f90d4ba954666e87d54", - "sha256:950b7ef08b2afdab2488ee2edaff92a03ca500a48f1e1aaa5900e73d6cf992bc", - "sha256:99c5a5bf7135607959441b7d720d96c8e5c46a1f96e9d6d4c9498be8d5f24212", - "sha256:b84ad94868e1e6a5e30d30ec419956042815dfaea1b1df1cef623e4564c374d9", - "sha256:bc3d14bf71a3fb94e5acf5bbf67331ab335467129af6416a437bd6024e4f743d", - "sha256:c2a80fd9a8d7e41b4e38ea9fe149deed0d6aaede255c497e66b8213274d6d61b", - "sha256:c44d3c82a933c6cbc21039326767e778eface44fca55c65719921c4b9661a3f7", - "sha256:cc31e906be1cc121ee201adbdf844522ea3349600dd0a40366611ca18cd40e81", - "sha256:d5d102e945ecca93bcd9801a7bb2fa703e37ad188a2f81b1e65e4abe4b51b00c", - "sha256:dd7936f2a6daa861143e376b3a1fb56e9b802f4980923594edd9ca5670974895", - "sha256:dee68ec462ff10c1d836c0ea2642116aba6151c6880b688e56b4c0246770f297", - "sha256:e76e78863a4eaec3aee5722d85d04dcbd9844bc6cd3bfa6aa880ff46ad16bfcb", - "sha256:eab51036cac2da8a50d7ff0ea30be47750547c9aa1aa2cf1a1b710a1827e7dbe", - "sha256:f4496d8d04da2e98cc9133e238ccebf6a13ef39a93da2e87146c8c8ac9768242", - "sha256:fbd3b5e18d34683decc00d9a360179ac1e7a320a5fee10ab8053ffd6deab76e0", - "sha256:feb24ff1226beeb056e247cf2e24bba5232519efb5645121c4aea5b6ad74c1f2" + "sha256:02f46fc0e3c5ac58b80d4d56eb0a7c7d97fcef69ace9326289fb9f1955e65cfe", + "sha256:0563c1b3826945eecd62186f3f5c7d31abb7391fedc893b7e2b26303b5a9f3fe", + "sha256:114b281e4d68302a324dd33abb04778e8557d88947875cbf4e842c2c01a030c5", + "sha256:14762875b22d0055f05d12abc7f7d61d5fd4fe4642ce1a249abdf8c700bf1fd8", + "sha256:15492a6368d985b76a2a5fdd2166cddfea5d24e69eefed4630cbaae5c81d89bd", + "sha256:17c073de315745a1510393a96e680d20af8e67e324f70b42accbd4cb3315c9fb", + "sha256:209b4a8ee987eccc91e2bd3ac36adee0e53a5970b8ac52c273f7f8fd4872c94c", + "sha256:230a8f7e24298dea47659251abc0fd8b3c4e38a664c59d4b89cca7f6c09c9e87", + "sha256:2e19413bf84934d651344783c9f5e22dee452e251cfd220ebadbed2d9931dbf0", + "sha256:393f389841e8f2dfc86f774ad22f00923fdee66d238af89b70ea314c4aefd290", + "sha256:3cf75f7cdc2397ed4442594b935a11ed5569961333d49b7539ea741be2cc79d5", + "sha256:3d78619672183be860b96ed96f533046ec97ca067fd46ac1f6a09cd9b7484287", + "sha256:40eced07f07a9e60e825554a31f923e8d3997cfc7fb31dbc1328c70826e04cde", + "sha256:493d3299ebe5f5a7c66b9819eacdcfbbaaf1a8e84911ddffcdc48888497afecf", + "sha256:4b302b45040890cea949ad092479e01ba25911a15e648429c7c5aae9650c67a8", + "sha256:515dfef7f869a0feb2afee66b957cc7bbe9ad0cdee45aec7fdc623f4ecd4fb16", + "sha256:547da6cacac20666422d4882cfcd51298d45f7ccb60a04ec27424d2f36ba3eaf", + "sha256:5df68496d19f849921f05f14f31bd6ef53ad4b00245da3195048c69934521809", + "sha256:64322071e046020e8797117b3658b9c2f80e3267daec409b350b6a7a05041213", + "sha256:7615dab56bb07bff74bc865307aeb89a8bfd9941d2ef9d817b9436da3a0ea54f", + "sha256:79ebfc238612123a713a457d92afb4096e2148be17df6c50fb9bf7a81c2f8013", + "sha256:7b18b97cf8ee5452fa5f4e3af95d01d84d86d32c5e2bfa260cf041749d66360b", + "sha256:932bb1ea39a54e9ea27fc9232163059a0b8855256f4052e776357ad9add6f1c9", + "sha256:a00bb73540af068ca7390e636c01cbc4f644961896fa9363154ff43fd37af2f5", + "sha256:a5ca29ee66f8343ed336816c553e82d6cade48a3ad702b9ffa6125d187e2dedb", + "sha256:af9aa9ef5ba1fd5b8c948bb11f44891968ab30356d65fd0cc6707d989cd521df", + "sha256:bb437315738aa441251214dad17428cafda9cdc9729499f1d6001748e1d432f4", + "sha256:bdb230b4943891321e06fc7def63c7aace16095be7d9cf3b1e01be2f10fba439", + "sha256:c6e9dcb4cb338d91a73f178d866d051efe7c62a7166653a91e7d9fb18274058f", + "sha256:cffe3ab27871bc3ea47df5d8f7013945712c46a3cc5a95b6bee15887f1675c22", + "sha256:d012ad7911653a906425d8473a1465caa9f8dea7fcf07b6d870397b774ea7c0f", + "sha256:d9e13b33afd39ddeb377eff2c1c4f00544e191e1d1dee5b6c51ddee8ea6f0cf5", + "sha256:e4b2b334e68b18ac9817d828ba44d8fcb391f6acb398bcc5062b14b2cbeac970", + "sha256:e54962802d4b8b18b6207d4a927032826af39395a3bd9196a5af43fc4e60b009", + "sha256:f705e12750171c0ab4ef2a3c76b9a4024a62c4103e3a55dd6f99265b9bc6fcfc", + "sha256:f881853d2643a29e643609da57b96d5f9c9b93f62429dcc1cbb413c7d07f0e1a", + "sha256:fe60131d21b31fd1a14bd43e6bb88256f69dfc3188b3a89d736d6c71ed43ec95" ], - "version": "==3.7.4" + "version": "==3.7.4.post0" }, "aioredis": { "hashes": [ @@ -116,18 +116,17 @@ }, "boto3": { "hashes": [ - "sha256:2219f1ebe88d266afa5516f993983eba8742b957fa4fd6854f3c73aa3030e931", - "sha256:c0d51f344b71656c2d395d2168600d91bea252a64fb5d503a955ea96426cde8b" + "sha256:64a8900b3a110e2d6ff4d87f4d8cd56f0c8527361d9fc9385fcb50efe7a4975a", + "sha256:8e9ff8006c41889ed8a11831dee62adf922e071f14d54c52946d1f7855ae7a8e" ], "index": "pypi", - "version": "==1.17.20" + "version": "==1.17.26" }, "botocore": { "hashes": [ - "sha256:80c32a81fb1ee8bdfa074a79bfb885bb2006e8a9782f2353c0c9f6392704e13a", - "sha256:e9e724b59278ebf5caf032be1e32bde0990d79e8052e3bbbb97b6c1d32feba28" + "sha256:4a785847a351e59f2329627fc9a19cf50f07644ea68996a1595d5a20487a423f" ], - "version": "==1.20.20" + "version": "==1.20.26" }, "cachetools": { "hashes": [ @@ -217,10 +216,10 @@ }, "chardet": { "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa", + "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5" ], - "version": "==3.0.4" + "version": "==4.0.0" }, "click": { "hashes": [ @@ -304,11 +303,11 @@ }, "defusedxml": { "hashes": [ - "sha256:6687150770438374ab581bb7a1b327a847dd9c5749e396102de3fad4e8a3ef93", - "sha256:f684034d135af4c6cbb949b8a4d2ed61634515257a67299e5f940fbaa34377f5" + "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", + "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61" ], "index": "pypi", - "version": "==0.6.0" + "version": "==0.7.1" }, "django": { "hashes": [ @@ -445,10 +444,10 @@ }, "google-auth": { "hashes": [ - "sha256:d3640ea61ee025d5af00e3ffd82ba0a06dd99724adaf50bdd52f49daf29f3f65", - "sha256:da5218cbf33b8461d7661d6b4ad91c12c0107e2767904d5e3ae6408031d5463e" + "sha256:63a5636d7eacfe6ef5b7e36e112b3149fa1c5b5ad77dd6df54910459bcd6b89f", + "sha256:d8958af6968e4ecd599f82357ebcfeb126f826ed0656126ad68416f810f7531e" ], - "version": "==1.27.0" + "version": "==1.27.1" }, "gunicorn": { "hashes": [ @@ -817,10 +816,10 @@ }, "prompt-toolkit": { "hashes": [ - "sha256:0fa02fa80363844a4ab4b8d6891f62dd0645ba672723130423ca4037b80c1974", - "sha256:62c811e46bd09130fb11ab759012a4ae385ce4fb2073442d1898867a824183bd" + "sha256:4cea7d09e46723885cb8bc54678175453e5071e9449821dce6f017b1d1fbfc1a", + "sha256:9397a7162cf45449147ad6042fa37983a081b8a73363a5253dd4072666333137" ], - "version": "==3.0.16" + "version": "==3.0.17" }, "psycopg2-binary": { "hashes": [ @@ -1024,15 +1023,23 @@ "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], "index": "pypi", "version": "==5.4.1" @@ -1069,10 +1076,47 @@ }, "ruamel.yaml": { "hashes": [ - "sha256:012b9470a0ea06e4e44e99e7920277edf6b46eee0232a04487ea73a7386340a5", - "sha256:076cc0bc34f1966d920a49f18b52b6ad559fbe656a0748e3535cf7b3f29ebf9e" + "sha256:64b06e7873eb8e1125525ecef7345447d786368cadca92a7cd9b59eae62e95a3", + "sha256:bb48c514222702878759a05af96f4b7ecdba9b33cd4efcf25c86b882cef3a942" ], - "version": "==0.16.12" + "version": "==0.16.13" + }, + "ruamel.yaml.clib": { + "hashes": [ + "sha256:058a1cc3df2a8aecc12f983a48bda99315cebf55a3b3a5463e37bb599b05727b", + "sha256:1236df55e0f73cd138c0eca074ee086136c3f16a97c2ac719032c050f7e0622f", + "sha256:1f8c0a4577c0e6c99d208de5c4d3fd8aceed9574bb154d7a2b21c16bb924154c", + "sha256:2602e91bd5c1b874d6f93d3086f9830f3e907c543c7672cf293a97c3fabdcd91", + "sha256:28116f204103cb3a108dfd37668f20abe6e3cafd0d3fd40dba126c732457b3cc", + "sha256:2d24bd98af676f4990c4d715bcdc2a60b19c56a3fb3a763164d2d8ca0e806ba7", + "sha256:2fd336a5c6415c82e2deb40d08c222087febe0aebe520f4d21910629018ab0f3", + "sha256:30dca9bbcbb1cc858717438218d11eafb78666759e5094dd767468c0d577a7e7", + "sha256:44c7b0498c39f27795224438f1a6be6c5352f82cb887bc33d962c3a3acc00df6", + "sha256:464e66a04e740d754170be5e740657a3b3b6d2bcc567f0c3437879a6e6087ff6", + "sha256:46d6d20815064e8bb023ea8628cfb7402c0f0e83de2c2227a88097e239a7dffd", + "sha256:4df5019e7783d14b79217ad9c56edf1ba7485d614ad5a385d1b3c768635c81c0", + "sha256:4e52c96ca66de04be42ea2278012a2342d89f5e82b4512fb6fb7134e377e2e62", + "sha256:5254af7d8bdf4d5484c089f929cb7f5bafa59b4f01d4f48adda4be41e6d29f99", + "sha256:52ae5739e4b5d6317b52f5b040b1b6639e8af68a5b8fd606a8b08658fbd0cab5", + "sha256:53b9dd1abd70e257a6e32f934ebc482dac5edb8c93e23deb663eac724c30b026", + "sha256:6c0a5dc52fc74eb87c67374a4e554d4761fd42a4d01390b7e868b30d21f4b8bb", + "sha256:73b3d43e04cc4b228fa6fa5d796409ece6fcb53a6c270eb2048109cbcbc3b9c2", + "sha256:74161d827407f4db9072011adcfb825b5258a5ccb3d2cd518dd6c9edea9e30f1", + "sha256:75f0ee6839532e52a3a53f80ce64925ed4aed697dd3fa890c4c918f3304bd4f4", + "sha256:839dd72545ef7ba78fd2aa1a5dd07b33696adf3e68fae7f31327161c1093001b", + "sha256:8be05be57dc5c7b4a0b24edcaa2f7275866d9c907725226cdde46da09367d923", + "sha256:8e8fd0a22c9d92af3a34f91e8a2594eeb35cba90ab643c5e0e643567dc8be43e", + "sha256:a873e4d4954f865dcb60bdc4914af7eaae48fb56b60ed6daa1d6251c72f5337c", + "sha256:ab845f1f51f7eb750a78937be9f79baea4a42c7960f5a94dde34e69f3cce1988", + "sha256:b1e981fe1aff1fd11627f531524826a4dcc1f26c726235a52fcb62ded27d150f", + "sha256:b4b0d31f2052b3f9f9b5327024dc629a253a83d8649d4734ca7f35b60ec3e9e5", + "sha256:c6ac7e45367b1317e56f1461719c853fd6825226f45b835df7436bb04031fd8a", + "sha256:daf21aa33ee9b351f66deed30a3d450ab55c14242cfdfcd377798e2c0d25c9f1", + "sha256:e9f7d1d8c26a6a12c23421061f9022bb62704e38211fe375c645485f38df34a2", + "sha256:f6061a31880c1ed6b6ce341215336e2f3d0c1deccd84957b6fa8ca474b41e89f" + ], + "markers": "platform_python_implementation == 'CPython' and python_version < '3.10'", + "version": "==0.2.2" }, "s3transfer": { "hashes": [ @@ -1765,15 +1809,23 @@ "sha256:5accb17103e43963b80e6f837831f38d314a0495500067cb25afab2e8d7a4018", "sha256:607774cbba28732bfa802b54baa7484215f530991055bb562efbed5b2f20a45e", "sha256:6c78645d400265a062508ae399b60b8c167bf003db364ecb26dcab2bda048253", + "sha256:72a01f726a9c7851ca9bfad6fd09ca4e090a023c00945ea05ba1638c09dc3347", "sha256:74c1485f7707cf707a7aef42ef6322b8f97921bd89be2ab6317fd782c2d53183", + "sha256:895f61ef02e8fed38159bb70f7e100e00f471eae2bc838cd0f4ebb21e28f8541", "sha256:8c1be557ee92a20f184922c7b6424e8ab6691788e6d86137c5d93c1a6ec1b8fb", "sha256:bb4191dfc9306777bc594117aee052446b3fa88737cd13b7188d0e7aa8162185", + "sha256:bfb51918d4ff3d77c1c856a9699f8492c612cde32fd3bcd344af9be34999bfdc", "sha256:c20cfa2d49991c8b4147af39859b167664f2ad4561704ee74c1de03318e898db", + "sha256:cb333c16912324fd5f769fff6bc5de372e9e7a202247b48870bc251ed40239aa", "sha256:d2d9808ea7b4af864f35ea216be506ecec180628aced0704e34aca0b040ffe46", + "sha256:d483ad4e639292c90170eb6f7783ad19490e7a8defb3e46f97dfe4bacae89122", "sha256:dd5de0646207f053eb0d6c74ae45ba98c3395a571a2891858e87df7c9b9bd51b", "sha256:e1d4970ea66be07ae37a3c2e48b5ec63f7ba6804bdddfdbd3cfd954d25a82e63", "sha256:e4fac90784481d221a8e4b1162afa7c47ed953be40d31ab4629ae917510051df", - "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc" + "sha256:fa5ae20527d8e831e8230cbffd9f8fe952815b2b7dae6ffec25318803a7528fc", + "sha256:fd7f6999a8070df521b6384004ef42833b9bd62cfee11a09bda1079b4b704247", + "sha256:fdc842473cd33f45ff6bce46aea678a54e3d21f1b61a7750ce3c498eedfe25d6", + "sha256:fe69978f3f768926cfa37b867e3843918e012cf83f680806599ddce33c2c68b0" ], "index": "pypi", "version": "==5.4.1" diff --git a/authentik/admin/api/metrics.py b/authentik/admin/api/metrics.py index d3e9f812e..ad3943e16 100644 --- a/authentik/admin/api/metrics.py +++ b/authentik/admin/api/metrics.py @@ -7,8 +7,8 @@ from django.db.models import Count, ExpressionWrapper, F, Model from django.db.models.fields import DurationField from django.db.models.functions import ExtractHour from django.utils.timezone import now -from drf_yasg2.utils import swagger_auto_schema -from rest_framework.fields import SerializerMethodField +from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method +from rest_framework.fields import IntegerField, SerializerMethodField from rest_framework.permissions import IsAdminUser from rest_framework.request import Request from rest_framework.response import Response @@ -37,23 +37,39 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]: for hour in range(0, -24, -1): results.append( { - "x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000, - "y": data[hour * -1], + "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) + * 1000, + "y_cord": data[hour * -1], } ) return results -class AdministrationMetricsSerializer(Serializer): +class CoordinateSerializer(Serializer): + """Coordinates for diagrams""" + + x_cord = IntegerField(read_only=True) + y_cord = IntegerField(read_only=True) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError + + +class LoginMetricsSerializer(Serializer): """Login Metrics per 1h""" logins_per_1h = SerializerMethodField() logins_failed_per_1h = SerializerMethodField() + @swagger_serializer_method(serializer_or_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) + @swagger_serializer_method(serializer_or_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) @@ -70,8 +86,8 @@ class AdministrationMetricsViewSet(ViewSet): permission_classes = [IsAdminUser] - @swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)}) + @swagger_auto_schema(responses={200: LoginMetricsSerializer(many=False)}) def list(self, request: Request) -> Response: """Login Metrics per 1h""" - serializer = AdministrationMetricsSerializer(True) + serializer = LoginMetricsSerializer(True) return Response(serializer.data) diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py index 3dca24bc1..d3abd5dea 100644 --- a/authentik/admin/api/tasks.py +++ b/authentik/admin/api/tasks.py @@ -25,8 +25,8 @@ class TaskSerializer(Serializer): task_finish_timestamp = DateTimeField(source="finish_timestamp") status = ChoiceField( - source="result.status.value", - choices=[(x.value, x.name) for x in TaskResultStatus], + source="result.status.name", + choices=[(x.name, x.name) for x in TaskResultStatus], ) messages = ListField(source="result.messages") diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 8ea2ec4ae..180f47969 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -1,4 +1,5 @@ """api v2 urls""" +from django.conf import settings from django.urls import path, re_path from drf_yasg2 import openapi from drf_yasg2.views import get_schema_view @@ -54,12 +55,24 @@ from authentik.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProvide from authentik.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet from authentik.sources.oauth.api import OAuthSourceViewSet from authentik.sources.saml.api import SAMLSourceViewSet -from authentik.stages.authenticator_static.api import AuthenticatorStaticStageViewSet -from authentik.stages.authenticator_totp.api import AuthenticatorTOTPStageViewSet +from authentik.stages.authenticator_static.api import ( + AuthenticatorStaticStageViewSet, + StaticAdminDeviceViewSet, + StaticDeviceViewSet, +) +from authentik.stages.authenticator_totp.api import ( + AuthenticatorTOTPStageViewSet, + TOTPAdminDeviceViewSet, + TOTPDeviceViewSet, +) from authentik.stages.authenticator_validate.api import ( AuthenticatorValidateStageViewSet, ) -from authentik.stages.authenticator_webauthn.api import AuthenticateWebAuthnStageViewSet +from authentik.stages.authenticator_webauthn.api import ( + AuthenticateWebAuthnStageViewSet, + WebAuthnAdminDeviceViewSet, + WebAuthnDeviceViewSet, +) from authentik.stages.captcha.api import CaptchaStageViewSet from authentik.stages.consent.api import ConsentStageViewSet from authentik.stages.deny.api import DenyStageViewSet @@ -133,6 +146,13 @@ router.register("propertymappings/ldap", LDAPPropertyMappingViewSet) router.register("propertymappings/saml", SAMLPropertyMappingViewSet) router.register("propertymappings/scope", ScopeMappingViewSet) +router.register("authenticators/static", StaticDeviceViewSet) +router.register("authenticators/totp", TOTPDeviceViewSet) +router.register("authenticators/webauthn", WebAuthnDeviceViewSet) +router.register("authenticators/admin/static", StaticAdminDeviceViewSet) +router.register("authenticators/admin/totp", TOTPAdminDeviceViewSet) +router.register("authenticators/admin/webauthn", WebAuthnAdminDeviceViewSet) + router.register("stages/all", StageViewSet) router.register("stages/authenticator/static", AuthenticatorStaticStageViewSet) router.register("stages/authenticator/totp", AuthenticatorTOTPStageViewSet) @@ -164,27 +184,26 @@ info = openapi.Info( name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE" ), ) -SchemaView = get_schema_view( - info, - public=True, - permission_classes=(AllowAny,), -) +SchemaView = get_schema_view(info, public=True, permission_classes=(AllowAny,)) -urlpatterns = [ - re_path( - r"^swagger(?P\.json|\.yaml)$", - SchemaView.without_ui(cache_timeout=0), - name="schema-json", - ), - path( - "swagger/", - SchemaView.with_ui("swagger", cache_timeout=0), - name="schema-swagger-ui", - ), - path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), +urlpatterns = router.urls + [ path( "flows/executor//", FlowExecutorView.as_view(), name="flow-executor", ), -] + router.urls + re_path( + r"^swagger(?P\.json|\.yaml)$", + SchemaView.without_ui(cache_timeout=0), + name="schema-json", + ), +] + +if settings.DEBUG: + urlpatterns = urlpatterns + [ + path( + "swagger/", + SchemaView.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + ] diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 76cc432b9..eae9fc986 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -2,6 +2,7 @@ from django.core.cache import cache from django.db.models import QuerySet from django.http.response import Http404 +from drf_yasg2.utils import swagger_auto_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action from rest_framework.fields import SerializerMethodField @@ -13,7 +14,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 get_events_per_1h +from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.core.api.providers import ProviderSerializer from authentik.core.models import Application from authentik.events.models import EventAction @@ -109,6 +110,7 @@ class ApplicationViewSet(ModelViewSet): serializer = self.get_serializer(allowed_applications, many=True) return self.get_paginated_response(serializer.data) + @swagger_auto_schema(responses={200: CoordinateSerializer(many=True)}) @action(detail=True) def metrics(self, request: Request, slug: str): """Metrics for application logins""" diff --git a/authentik/core/api/groups.py b/authentik/core/api/groups.py index 152c1ea9f..2ca259fb4 100644 --- a/authentik/core/api/groups.py +++ b/authentik/core/api/groups.py @@ -21,3 +21,4 @@ class GroupViewSet(ModelViewSet): serializer_class = GroupSerializer search_fields = ["name", "is_superuser"] filterset_fields = ["name", "is_superuser"] + ordering = ["name"] diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index b93f75155..438efa2df 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -28,9 +28,9 @@ class MetaNameSerializer(Serializer): class TypeCreateSerializer(Serializer): """Types of an object that can be created""" - name = CharField(read_only=True) - description = CharField(read_only=True) - link = CharField(read_only=True) + name = CharField(required=True) + description = CharField(required=True) + link = CharField(required=True) def create(self, validated_data: dict) -> Model: raise NotImplementedError diff --git a/authentik/core/templates/generic/delete.html b/authentik/core/templates/generic/delete.html index 234dab40e..8f8a58c9c 100644 --- a/authentik/core/templates/generic/delete.html +++ b/authentik/core/templates/generic/delete.html @@ -1,9 +1,6 @@ -{% extends container_template|default:"administration/base.html" %} - {% load i18n %} {% load authentik_utils %} -{% block content %}
{% block above_form %} @@ -38,4 +35,3 @@ {% trans "Back" %} -{% endblock %} diff --git a/authentik/core/views/user.py b/authentik/core/views/user.py index 8554a9bc1..60286f2cb 100644 --- a/authentik/core/views/user.py +++ b/authentik/core/views/user.py @@ -7,6 +7,7 @@ from django.contrib.auth.mixins import ( ) from django.contrib.messages.views import SuccessMessageMixin from django.http.response import HttpResponse +from django.urls import reverse_lazy from django.utils.translation import gettext as _ from django.views.generic import UpdateView from django.views.generic.base import TemplateView @@ -34,7 +35,7 @@ class UserDetailsView(SuccessMessageMixin, LoginRequiredMixin, UpdateView): form_class = UserDetailForm success_message = _("Successfully updated user.") - success_url = "/" + success_url = reverse_lazy("authentik_core:user-details") def get_object(self): return self.request.user diff --git a/authentik/events/api/notification.py b/authentik/events/api/notification.py index 9e7aef716..9908533ca 100644 --- a/authentik/events/api/notification.py +++ b/authentik/events/api/notification.py @@ -13,7 +13,7 @@ class NotificationSerializer(ModelSerializer): body = ReadOnlyField() severity = ReadOnlyField() - event = EventSerializer() + event = EventSerializer(required=False) class Meta: diff --git a/authentik/events/api/notification_transport.py b/authentik/events/api/notification_transport.py index b715285ad..e951f2a9f 100644 --- a/authentik/events/api/notification_transport.py +++ b/authentik/events/api/notification_transport.py @@ -1,11 +1,12 @@ """NotificationTransport API Views""" from django.http.response import Http404 +from drf_yasg2.utils import no_body, swagger_auto_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action -from rest_framework.fields import SerializerMethodField +from rest_framework.fields import CharField, ListField, SerializerMethodField from rest_framework.request import Request from rest_framework.response import Response -from rest_framework.serializers import ModelSerializer +from rest_framework.serializers import ModelSerializer, Serializer from rest_framework.viewsets import ModelViewSet from authentik.events.models import ( @@ -38,12 +39,28 @@ class NotificationTransportSerializer(ModelSerializer): ] +class NotificationTransportTestSerializer(Serializer): + """Notification test serializer""" + + messages = ListField(child=CharField()) + + def create(self, request: Request) -> Response: + raise NotImplementedError + + def update(self, request: Request) -> Response: + raise NotImplementedError + + class NotificationTransportViewSet(ModelViewSet): """NotificationTransport Viewset""" queryset = NotificationTransport.objects.all() serializer_class = NotificationTransportSerializer + @swagger_auto_schema( + responses={200: NotificationTransportTestSerializer(many=False)}, + request_body=no_body, + ) @action(detail=True, methods=["post"]) # pylint: disable=invalid-name def test(self, request: Request, pk=None) -> Response: @@ -61,6 +78,10 @@ class NotificationTransportViewSet(ModelViewSet): user=request.user, ) try: - return Response(transport.send(notification)) + response = NotificationTransportTestSerializer( + data={"messages": transport.send(notification)} + ) + response.is_valid() + return Response(response.data) except NotificationTransportError as exc: return Response(str(exc.__cause__ or None), status=503) diff --git a/authentik/events/geo.py b/authentik/events/geo.py index b249a562e..f7e2f90cb 100644 --- a/authentik/events/geo.py +++ b/authentik/events/geo.py @@ -12,7 +12,10 @@ def get_geoip_reader() -> Optional[Reader]: path = CONFIG.y("authentik.geoip") if path == "" or not path: return None - return Reader(path) + try: + return Reader(path) + except OSError: + return None GEOIP_READER = get_geoip_reader() diff --git a/authentik/flows/challenge.py b/authentik/flows/challenge.py index 9f45da474..0abf925d3 100644 --- a/authentik/flows/challenge.py +++ b/authentik/flows/challenge.py @@ -38,7 +38,9 @@ class Challenge(Serializer): """Challenge that gets sent to the client based on which stage is currently active""" - type = ChoiceField(choices=list(ChallengeTypes)) + type = ChoiceField( + choices=[(x.name, x.name) for x in ChallengeTypes], + ) component = CharField(required=False) title = CharField(required=False) @@ -90,7 +92,7 @@ class ChallengeResponse(Serializer): stage: Optional["StageView"] - def __init__(self, instance, data, **kwargs): + def __init__(self, instance=None, data=None, **kwargs): self.stage = kwargs.pop("stage", None) super().__init__(instance=instance, data=data, **kwargs) diff --git a/authentik/flows/stage.py b/authentik/flows/stage.py index c55524191..59265d8aa 100644 --- a/authentik/flows/stage.py +++ b/authentik/flows/stage.py @@ -4,6 +4,7 @@ from django.http import HttpRequest from django.http.request import QueryDict from django.http.response import HttpResponse from django.views.generic.base import View +from rest_framework.request import Request from structlog.stdlib import get_logger from authentik.core.models import DEFAULT_AVATAR, User @@ -67,9 +68,9 @@ class ChallengeStageView(StageView): return HttpChallengeResponse(challenge) # pylint: disable=unused-argument - def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + def post(self, request: Request, *args, **kwargs) -> HttpResponse: """Handle challenge response""" - challenge: ChallengeResponse = self.get_response_instance(data=request.POST) + challenge: ChallengeResponse = self.get_response_instance(data=request.data) if not challenge.is_valid(): return self.challenge_invalid(challenge) return self.challenge_valid(challenge) diff --git a/authentik/flows/views.py b/authentik/flows/views.py index 626dfe005..c44d1708f 100644 --- a/authentik/flows/views.py +++ b/authentik/flows/views.py @@ -9,11 +9,16 @@ from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views.decorators.clickjacking import xframe_options_sameorigin from django.views.generic import TemplateView, View +from drf_yasg2.utils import no_body, swagger_auto_schema +from rest_framework.permissions import AllowAny +from rest_framework.views import APIView from structlog.stdlib import BoundLogger, get_logger from authentik.core.models import USER_ATTRIBUTE_DEBUG from authentik.events.models import cleanse_dict from authentik.flows.challenge import ( + Challenge, + ChallengeResponse, ChallengeTypes, HttpChallengeResponse, RedirectChallenge, @@ -40,9 +45,11 @@ SESSION_KEY_GET = "authentik_flows_get" @method_decorator(xframe_options_sameorigin, name="dispatch") -class FlowExecutorView(View): +class FlowExecutorView(APIView): """Stage 1 Flow executor, passing requests to Stage Views""" + permission_classes = [AllowAny] + flow: Flow plan: Optional[FlowPlan] = None @@ -113,8 +120,13 @@ class FlowExecutorView(View): self.current_stage_view.request = request return super().dispatch(request) + @swagger_auto_schema( + responses={200: Challenge()}, + request_body=no_body, + operation_id="flows_executor_get", + ) def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - """pass get request to current stage""" + """Get the next pending challenge from the currently active flow.""" self._logger.debug( "f(exec): Passing GET", view_class=class_to_path(self.current_stage_view.__class__), @@ -127,8 +139,13 @@ class FlowExecutorView(View): self._logger.exception(exc) return to_stage_response(request, FlowErrorResponse(request, exc)) + @swagger_auto_schema( + responses={200: Challenge()}, + request_body=ChallengeResponse(), + operation_id="flows_executor_solve", + ) def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - """pass post request to current stage""" + """Solve the previously retrieved challenge and advanced to the next stage.""" self._logger.debug( "f(exec): Passing POST", view_class=class_to_path(self.current_stage_view.__class__), @@ -175,8 +192,10 @@ class FlowExecutorView(View): "f(exec): Continuing with next stage", reamining=len(self.plan.stages), ) + kwargs = self.kwargs + kwargs.update({"flow_slug": self.flow.slug}) return redirect_with_qs( - "authentik_api:flow-executor", self.request.GET, **self.kwargs + "authentik_api:flow-executor", self.request.GET, **kwargs ) # User passed all stages self._logger.debug( diff --git a/authentik/lib/default.yml b/authentik/lib/default.yml index 3bb72cf77..27be37620 100644 --- a/authentik/lib/default.yml +++ b/authentik/lib/default.yml @@ -13,6 +13,7 @@ redis: ws_db: 2 debug: false + log_level: info # Error reporting, sends stacktrace to sentry.beryju.org diff --git a/authentik/outposts/channels.py b/authentik/outposts/channels.py index 7ad5b0dfa..d104e1f16 100644 --- a/authentik/outposts/channels.py +++ b/authentik/outposts/channels.py @@ -55,13 +55,21 @@ class OutpostConsumer(AuthJsonConsumer): OutpostState( uid=self.channel_name, last_seen=datetime.now(), _outpost=self.outpost ).save(timeout=OUTPOST_HELLO_INTERVAL * 1.5) - LOGGER.debug("added channel to cache", channel_name=self.channel_name) + LOGGER.debug( + "added outpost instace to cache", + outpost=self.outpost, + channel_name=self.channel_name, + ) # pylint: disable=unused-argument def disconnect(self, close_code): if self.outpost: OutpostState.for_channel(self.outpost, self.channel_name).delete() - LOGGER.debug("removed channel from cache", channel_name=self.channel_name) + LOGGER.debug( + "removed outpost instance from cache", + outpost=self.outpost, + channel_name=self.channel_name, + ) def receive_json(self, content: Data): msg = from_dict(WebsocketMessage, content) diff --git a/authentik/policies/signals.py b/authentik/policies/signals.py index 3fabc7202..fabde968f 100644 --- a/authentik/policies/signals.py +++ b/authentik/policies/signals.py @@ -26,5 +26,5 @@ def invalidate_policy_cache(sender, instance, **_): cache.delete_many(keys) LOGGER.debug("Invalidating policy cache", policy=instance, keys=total) # Also delete user application cache - keys = cache.keys(user_app_cache_key("*")) + keys = cache.keys(user_app_cache_key("*")) or [] cache.delete_many(keys) diff --git a/authentik/providers/saml/views/flows.py b/authentik/providers/saml/views/flows.py index 2cd2758bc..eb03eb9c8 100644 --- a/authentik/providers/saml/views/flows.py +++ b/authentik/providers/saml/views/flows.py @@ -74,7 +74,7 @@ class SAMLFlowFinalView(ChallengeStageView): return super().get( self.request, **{ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-autosubmit", "title": "Redirecting to %(app)s..." % {"app": application.name}, "url": provider.acs_url, diff --git a/authentik/root/settings.py b/authentik/root/settings.py index abfe9485a..3e1e534f2 100644 --- a/authentik/root/settings.py +++ b/authentik/root/settings.py @@ -150,7 +150,6 @@ SWAGGER_SETTINGS = { REST_FRAMEWORK = { "DEFAULT_PAGINATION_CLASS": "authentik.api.pagination.Pagination", "PAGE_SIZE": 100, - "DATETIME_FORMAT": "%s", "DEFAULT_FILTER_BACKENDS": [ "rest_framework_guardian.filters.ObjectPermissionsFilter", "django_filters.rest_framework.DjangoFilterBackend", diff --git a/authentik/sources/oauth/forms.py b/authentik/sources/oauth/forms.py index 69ee0e41d..c8190882b 100644 --- a/authentik/sources/oauth/forms.py +++ b/authentik/sources/oauth/forms.py @@ -15,9 +15,11 @@ class OAuthSourceForm(forms.ModelForm): self.fields["authentication_flow"].queryset = Flow.objects.filter( designation=FlowDesignation.AUTHENTICATION ) + self.fields["authentication_flow"].required = True self.fields["enrollment_flow"].queryset = Flow.objects.filter( designation=FlowDesignation.ENROLLMENT ) + self.fields["enrollment_flow"].required = True if hasattr(self.Meta, "overrides"): for overide_field, overide_value in getattr(self.Meta, "overrides").items(): self.fields[overide_field].initial = overide_value diff --git a/authentik/sources/oauth/views/callback.py b/authentik/sources/oauth/views/callback.py index 17d3ad8bd..54a294826 100644 --- a/authentik/sources/oauth/views/callback.py +++ b/authentik/sources/oauth/views/callback.py @@ -4,6 +4,7 @@ from typing import Any, Optional from django.conf import settings from django.contrib import messages from django.http import Http404, HttpRequest, HttpResponse +from django.http.response import HttpResponseBadRequest from django.shortcuts import redirect from django.urls import reverse from django.utils.translation import gettext as _ @@ -151,6 +152,8 @@ class OAuthCallback(OAuthClientMixin, View): PLAN_CONTEXT_REDIRECT: final_redirect, } ) + if not flow: + return HttpResponseBadRequest() # We run the Flow planner here so we can pass the Pending user in the context planner = FlowPlanner(flow) plan = planner.plan(self.request, kwargs) @@ -233,6 +236,9 @@ class OAuthCallback(OAuthClientMixin, View): PLAN_CONTEXT_SOURCES_OAUTH_ACCESS: access, } # We run the Flow planner here so we can pass the Pending user in the context + if not source.enrollment_flow: + LOGGER.warning("source has no enrollment flow", source=source) + return HttpResponseBadRequest() planner = FlowPlanner(source.enrollment_flow) plan = planner.plan(self.request, context) plan.append(in_memory_stage(PostUserEnrollmentStage)) diff --git a/authentik/stages/authenticator_static/api.py b/authentik/stages/authenticator_static/api.py index 8ac6fc8c6..c32ed5443 100644 --- a/authentik/stages/authenticator_static/api.py +++ b/authentik/stages/authenticator_static/api.py @@ -1,5 +1,8 @@ """AuthenticatorStaticStage API Views""" -from rest_framework.viewsets import ModelViewSet +from django_otp.plugins.otp_static.models import StaticDevice +from rest_framework.permissions import IsAdminUser +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from authentik.flows.api.stages import StageSerializer from authentik.stages.authenticator_static.models import AuthenticatorStaticStage @@ -19,3 +22,39 @@ class AuthenticatorStaticStageViewSet(ModelViewSet): queryset = AuthenticatorStaticStage.objects.all() serializer_class = AuthenticatorStaticStageSerializer + + +class StaticDeviceSerializer(ModelSerializer): + """Serializer for static authenticator devices""" + + class Meta: + + model = StaticDevice + fields = ["name", "token_set"] + depth = 2 + + +class StaticDeviceViewSet(ModelViewSet): + """Viewset for static authenticator devices""" + + queryset = StaticDevice.objects.none() + serializer_class = StaticDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] + + def get_queryset(self): + if not self.request: + return super().get_queryset() + return StaticDevice.objects.filter(user=self.request.user) + + +class StaticAdminDeviceViewSet(ReadOnlyModelViewSet): + """Viewset for static authenticator devices (for admins)""" + + permission_classes = [IsAdminUser] + queryset = StaticDevice.objects.all() + serializer_class = StaticDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] diff --git a/authentik/stages/authenticator_static/stage.py b/authentik/stages/authenticator_static/stage.py index caa5ad6d2..f0a834464 100644 --- a/authentik/stages/authenticator_static/stage.py +++ b/authentik/stages/authenticator_static/stage.py @@ -31,7 +31,7 @@ class AuthenticatorStaticStageView(ChallengeStageView): tokens: list[StaticToken] = self.request.session[SESSION_STATIC_TOKENS] return AuthenticatorStaticChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-authenticator-static", "codes": [token.token for token in tokens], } diff --git a/authentik/stages/authenticator_totp/api.py b/authentik/stages/authenticator_totp/api.py index b7e40fc6c..c5a093221 100644 --- a/authentik/stages/authenticator_totp/api.py +++ b/authentik/stages/authenticator_totp/api.py @@ -1,5 +1,8 @@ """AuthenticatorTOTPStage API Views""" -from rest_framework.viewsets import ModelViewSet +from django_otp.plugins.otp_totp.models import TOTPDevice +from rest_framework.permissions import IsAdminUser +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from authentik.flows.api.stages import StageSerializer from authentik.stages.authenticator_totp.models import AuthenticatorTOTPStage @@ -19,3 +22,41 @@ class AuthenticatorTOTPStageViewSet(ModelViewSet): queryset = AuthenticatorTOTPStage.objects.all() serializer_class = AuthenticatorTOTPStageSerializer + + +class TOTPDeviceSerializer(ModelSerializer): + """Serializer for totp authenticator devices""" + + class Meta: + + model = TOTPDevice + fields = [ + "name", + ] + depth = 2 + + +class TOTPDeviceViewSet(ModelViewSet): + """Viewset for totp authenticator devices""" + + queryset = TOTPDevice.objects.none() + serializer_class = TOTPDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] + + def get_queryset(self): + if not self.request: + return super().get_queryset() + return TOTPDevice.objects.filter(user=self.request.user) + + +class TOTPAdminDeviceViewSet(ReadOnlyModelViewSet): + """Viewset for totp authenticator devices (for admins)""" + + permission_classes = [IsAdminUser] + queryset = TOTPDevice.objects.all() + serializer_class = TOTPDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] diff --git a/authentik/stages/authenticator_totp/stage.py b/authentik/stages/authenticator_totp/stage.py index 7c51e4993..060b1b6f8 100644 --- a/authentik/stages/authenticator_totp/stage.py +++ b/authentik/stages/authenticator_totp/stage.py @@ -51,7 +51,7 @@ class AuthenticatorTOTPStageView(ChallengeStageView): device: TOTPDevice = self.request.session[SESSION_TOTP_DEVICE] return AuthenticatorTOTPChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-authenticator-totp", "config_url": device.config_url, } diff --git a/authentik/stages/authenticator_validate/stage.py b/authentik/stages/authenticator_validate/stage.py index a1facfa3f..4e2e9f86a 100644 --- a/authentik/stages/authenticator_validate/stage.py +++ b/authentik/stages/authenticator_validate/stage.py @@ -145,7 +145,7 @@ class AuthenticatorValidateStageView(ChallengeStageView): challenges = self.request.session["device_challenges"] return AuthenticatorChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-authenticator-validate", "device_challenges": challenges, } diff --git a/authentik/stages/authenticator_webauthn/api.py b/authentik/stages/authenticator_webauthn/api.py index 6fcebd119..9ee607887 100644 --- a/authentik/stages/authenticator_webauthn/api.py +++ b/authentik/stages/authenticator_webauthn/api.py @@ -1,8 +1,13 @@ """AuthenticateWebAuthnStage API Views""" -from rest_framework.viewsets import ModelViewSet +from rest_framework.permissions import IsAdminUser +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet from authentik.flows.api.stages import StageSerializer -from authentik.stages.authenticator_webauthn.models import AuthenticateWebAuthnStage +from authentik.stages.authenticator_webauthn.models import ( + AuthenticateWebAuthnStage, + WebAuthnDevice, +) class AuthenticateWebAuthnStageSerializer(StageSerializer): @@ -19,3 +24,41 @@ class AuthenticateWebAuthnStageViewSet(ModelViewSet): queryset = AuthenticateWebAuthnStage.objects.all() serializer_class = AuthenticateWebAuthnStageSerializer + + +class WebAuthnDeviceSerializer(ModelSerializer): + """Serializer for WebAuthn authenticator devices""" + + class Meta: + + model = WebAuthnDevice + fields = [ + "name", + ] + depth = 2 + + +class WebAuthnDeviceViewSet(ModelViewSet): + """Viewset for WebAuthn authenticator devices""" + + queryset = WebAuthnDevice.objects.none() + serializer_class = WebAuthnDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] + + def get_queryset(self): + if not self.request: + return super().get_queryset() + return WebAuthnDevice.objects.filter(user=self.request.user) + + +class WebAuthnAdminDeviceViewSet(ReadOnlyModelViewSet): + """Viewset for WebAuthn authenticator devices (for admins)""" + + permission_classes = [IsAdminUser] + queryset = WebAuthnDevice.objects.all() + serializer_class = WebAuthnDeviceSerializer + search_fields = ["name"] + filterset_fields = ["name"] + ordering = ["name"] diff --git a/authentik/stages/authenticator_webauthn/stage.py b/authentik/stages/authenticator_webauthn/stage.py index 71218b5f8..f4d20b7d8 100644 --- a/authentik/stages/authenticator_webauthn/stage.py +++ b/authentik/stages/authenticator_webauthn/stage.py @@ -122,7 +122,7 @@ class AuthenticatorWebAuthnStageView(ChallengeStageView): return AuthenticatorWebAuthnChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-authenticator-webauthn", "registration": make_credential_options.registration_dict, } diff --git a/authentik/stages/captcha/stage.py b/authentik/stages/captcha/stage.py index 4254c8b94..8b26a054c 100644 --- a/authentik/stages/captcha/stage.py +++ b/authentik/stages/captcha/stage.py @@ -63,7 +63,7 @@ class CaptchaStageView(ChallengeStageView): def get_challenge(self, *args, **kwargs) -> Challenge: return CaptchaChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-captcha", "site_key": self.executor.current_stage.public_key, } diff --git a/authentik/stages/consent/stage.py b/authentik/stages/consent/stage.py index 6b30cd078..c7d72ae31 100644 --- a/authentik/stages/consent/stage.py +++ b/authentik/stages/consent/stage.py @@ -38,7 +38,7 @@ class ConsentStageView(ChallengeStageView): def get_challenge(self) -> Challenge: challenge = ConsentChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-consent", } ) diff --git a/authentik/stages/dummy/stage.py b/authentik/stages/dummy/stage.py index cf280d444..10f5ad3ca 100644 --- a/authentik/stages/dummy/stage.py +++ b/authentik/stages/dummy/stage.py @@ -24,7 +24,7 @@ class DummyStageView(ChallengeStageView): def get_challenge(self, *args, **kwargs) -> Challenge: return DummyChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "", "title": self.executor.current_stage.name, } diff --git a/authentik/stages/email/stage.py b/authentik/stages/email/stage.py index 7c398ccb0..de3b2d394 100644 --- a/authentik/stages/email/stage.py +++ b/authentik/stages/email/stage.py @@ -94,7 +94,7 @@ class EmailStageView(ChallengeStageView): def get_challenge(self) -> Challenge: challenge = EmailChallenge( - data={"type": ChallengeTypes.native, "component": "ak-stage-email"} + data={"type": ChallengeTypes.native.value, "component": "ak-stage-email"} ) return challenge diff --git a/authentik/stages/identification/stage.py b/authentik/stages/identification/stage.py index 61f99cb3b..a9365f515 100644 --- a/authentik/stages/identification/stage.py +++ b/authentik/stages/identification/stage.py @@ -78,7 +78,7 @@ class IdentificationStageView(ChallengeStageView): current_stage: IdentificationStage = self.executor.current_stage challenge = IdentificationChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-identification", "primary_action": _("Log in"), "input_type": "text", diff --git a/authentik/stages/password/stage.py b/authentik/stages/password/stage.py index b575c99f3..497c7d32c 100644 --- a/authentik/stages/password/stage.py +++ b/authentik/stages/password/stage.py @@ -78,7 +78,7 @@ class PasswordStageView(ChallengeStageView): def get_challenge(self) -> Challenge: challenge = PasswordChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-password", } ) diff --git a/authentik/stages/prompt/stage.py b/authentik/stages/prompt/stage.py index d6ca1e65b..6d895e659 100644 --- a/authentik/stages/prompt/stage.py +++ b/authentik/stages/prompt/stage.py @@ -164,7 +164,7 @@ class PromptStageView(ChallengeStageView): fields = list(self.executor.current_stage.fields.all().order_by("order")) challenge = PromptChallenge( data={ - "type": ChallengeTypes.native, + "type": ChallengeTypes.native.value, "component": "ak-stage-prompt", "fields": [PromptSerializer(field).data for field in fields], }, diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c38dc075e..596ce8461 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -279,6 +279,7 @@ stages: displayName: Build static files for e2e inputs: script: | + docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true cd web npm i npm run build diff --git a/docker-compose.yml b/docker-compose.yml index 9aff43aae..f05eceb82 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -19,7 +19,7 @@ services: networks: - internal server: - image: beryju/authentik:${AUTHENTIK_TAG:-2021.3.3} + image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.3.3} command: server environment: AUTHENTIK_REDIS__HOST: redis @@ -27,9 +27,11 @@ services: AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + # AUTHENTIK_ERROR_REPORTING__ENABLED: true volumes: - ./media:/media - ./custom-templates:/templates + - geoip:/geoip ports: - 8000 networks: @@ -45,7 +47,7 @@ services: env_file: - .env worker: - image: beryju/authentik:${AUTHENTIK_TAG:-2021.3.3} + image: ${AUTHENTIK_IMAGE:-beryju/authentik}:${AUTHENTIK_TAG:-2021.3.3} command: worker networks: - internal @@ -55,14 +57,16 @@ services: AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik} AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik} AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS} + # AUTHENTIK_ERROR_REPORTING__ENABLED: true volumes: - ./backups:/backups - /var/run/docker.sock:/var/run/docker.sock - ./custom-templates:/templates + - geoip:/geoip env_file: - .env static: - image: beryju/authentik-static:${AUTHENTIK_TAG:-2021.3.3} + image: ${AUTHENTIK_IMAGE_STATIC:-beryju/authentik-static}:${AUTHENTIK_TAG:-2021.3.3} networks: - internal labels: @@ -91,10 +95,21 @@ services: - "127.0.0.1:8080:8080" networks: - internal + geoipupdate: + image: "maxmindinc/geoipupdate:latest" + volumes: + - "geoip:/usr/share/GeoIP" + environment: + GEOIPUPDATE_EDITION_IDS: "GeoLite2-City" + GEOIPUPDATE_FREQUENCY: "8" + env_file: + - .env volumes: database: driver: local + geoip: + driver: local networks: internal: {} diff --git a/helm/README.md b/helm/README.md index f0670fd2e..9da2dc379 100644 --- a/helm/README.md +++ b/helm/README.md @@ -4,7 +4,7 @@ |-----------------------------------|-------------------------|-------------| | image.name | beryju/authentik | Image used to run the authentik server and worker | | image.name_static | beryju/authentik-static | Image used to run the authentik static server (CSS and JS Files) | -| image.tag | 2021.3.3 | Image tag | +| image.tag | 2021.3.3 | Image tag | | image.pullPolicy | IfNotPresent | Image Pull Policy used for all deployments | | serverReplicas | 1 | Replicas for the Server deployment | | workerReplicas | 1 | Replicas for the Worker deployment | @@ -22,6 +22,10 @@ | config.email.use_ssl | false | Enable SSL | | config.email.timeout | 10 | SMTP Timeout | | config.email.from | authentik@localhost | Email address authentik will send from, should have a correct @domain | +| geoip.enabled | false | Optionally enable GeoIP | +| geoip.accountId | | GeoIP MaxMind Account ID | +| geoip.licenseKey | | GeoIP MaxMind License key | +| geoip.image | maxmindinc/geoipupdate:latest | GeoIP Updater image | | backup.accessKey | | Optionally enable S3 Backup, Access Key | | backup.secretKey | | Optionally enable S3 Backup, Secret Key | | backup.bucket | | Optionally enable S3 Backup, Bucket | diff --git a/helm/templates/geoip-configmap.yaml b/helm/templates/geoip-configmap.yaml new file mode 100644 index 000000000..3e6dd47f3 --- /dev/null +++ b/helm/templates/geoip-configmap.yaml @@ -0,0 +1,11 @@ +{{- if .Values.geoip.enabled -}} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "authentik.fullname" . }}-geoip-config +data: + GEOIPUPDATE_ACCOUNT_ID: "{{ .Values.geoip.accountId }}" + GEOIPUPDATE_LICENSE_KEY: "{{ .Values.geoip.licenseKey }}" + GEOIPUPDATE_EDITION_IDS: "GeoLite2-City" + GEOIPUPDATE_FREQUENCY: "8" +{{- end }} diff --git a/helm/templates/geoip-deployment.yaml b/helm/templates/geoip-deployment.yaml new file mode 100644 index 000000000..fb7088245 --- /dev/null +++ b/helm/templates/geoip-deployment.yaml @@ -0,0 +1,39 @@ +{{- if .Values.geoip.enabled -}} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "authentik.fullname" . }}-geoip + labels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + helm.sh/chart: {{ include "authentik.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + k8s.goauthentik.io/component: geoip +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + k8s.goauthentik.io/component: geoip + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + k8s.goauthentik.io/component: geoip + spec: + containers: + - name: geoip + image: "{{ .Values.geoip.image }}" + envFrom: + - configMapRef: + name: {{ include "authentik.fullname" . }}-geoip-config + volumeMounts: + - name: geoip + mountPath: /usr/share/GeoIP + volumes: + - name: geoip + persistentVolumeClaim: + claimName: {{ include "authentik.fullname" . }}-geoip +{{- end }} diff --git a/helm/templates/geoip-pvc.yaml b/helm/templates/geoip-pvc.yaml new file mode 100644 index 000000000..2cd0caa14 --- /dev/null +++ b/helm/templates/geoip-pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.geoip.enabled -}} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "authentik.fullname" . }}-geoip + labels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + helm.sh/chart: {{ include "authentik.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + accessModes: + - ReadWriteMany + resources: + requests: + storage: 1Gi +{{- end }} diff --git a/helm/templates/prom-rules.yaml b/helm/templates/prom-rules.yaml new file mode 100644 index 000000000..847f2a7e7 --- /dev/null +++ b/helm/templates/prom-rules.yaml @@ -0,0 +1,121 @@ +{{- if .Values.monitoring.enabled -}} +--- +apiVersion: monitoring.coreos.com/v1 +kind: PrometheusRule +metadata: + name: {{ include "authentik.fullname" . }}-static-rules + labels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + helm.sh/chart: {{ include "authentik.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} +spec: + groups: + - name: Aggregate request counters + rules: + - record: job:django_http_requests_before_middlewares_total:sum_rate30s + expr: sum(rate(django_http_requests_before_middlewares_total[30s])) by (job) + - record: job:django_http_requests_unknown_latency_total:sum_rate30s + expr: sum(rate(django_http_requests_unknown_latency_total[30s])) by (job) + - record: job:django_http_ajax_requests_total:sum_rate30s + expr: sum(rate(django_http_ajax_requests_total[30s])) by (job) + - record: job:django_http_responses_before_middlewares_total:sum_rate30s + expr: sum(rate(django_http_responses_before_middlewares_total[30s])) by (job) + - record: job:django_http_requests_unknown_latency_including_middlewares_total:sum_rate30s + expr: sum(rate(django_http_requests_unknown_latency_including_middlewares_total[30s])) by (job) + - record: job:django_http_requests_body_total_bytes:sum_rate30s + expr: sum(rate(django_http_requests_body_total_bytes[30s])) by (job) + - record: job:django_http_responses_streaming_total:sum_rate30s + expr: sum(rate(django_http_responses_streaming_total[30s])) by (job) + - record: job:django_http_responses_body_total_bytes:sum_rate30s + expr: sum(rate(django_http_responses_body_total_bytes[30s])) by (job) + - record: job:django_http_requests_total:sum_rate30s + expr: sum(rate(django_http_requests_total_by_method[30s])) by (job) + - record: job:django_http_requests_total_by_method:sum_rate30s + expr: sum(rate(django_http_requests_total_by_method[30s])) by (job,method) + - record: job:django_http_requests_total_by_transport:sum_rate30s + expr: sum(rate(django_http_requests_total_by_transport[30s])) by (job,transport) + - record: job:django_http_requests_total_by_view:sum_rate30s + expr: sum(rate(django_http_requests_total_by_view_transport_method[30s])) by (job,view) + - record: job:django_http_requests_total_by_view_transport_method:sum_rate30s + expr: sum(rate(django_http_requests_total_by_view_transport_method[30s])) by (job,view,transport,method) + - record: job:django_http_responses_total_by_templatename:sum_rate30s + expr: sum(rate(django_http_responses_total_by_templatename[30s])) by (job,templatename) + - record: job:django_http_responses_total_by_status:sum_rate30s + expr: sum(rate(django_http_responses_total_by_status[30s])) by (job,status) + - record: job:django_http_responses_total_by_status_name_method:sum_rate30s + expr: sum(rate(django_http_responses_total_by_status_name_method[30s])) by (job,status,name,method) + - record: job:django_http_responses_total_by_charset:sum_rate30s + expr: sum(rate(django_http_responses_total_by_charset[30s])) by (job,charset) + - record: job:django_http_exceptions_total_by_type:sum_rate30s + expr: sum(rate(django_http_exceptions_total_by_type[30s])) by (job,type) + - record: job:django_http_exceptions_total_by_view:sum_rate30s + expr: sum(rate(django_http_exceptions_total_by_view[30s])) by (job,view) + - name: Aggregate latency histograms + rules: + - record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s + expr: histogram_quantile(0.50, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "50" + - record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s + expr: histogram_quantile(0.95, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "95" + - record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s + expr: histogram_quantile(0.99, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "99" + - record: job:django_http_requests_latency_including_middlewares_seconds:quantile_rate30s + expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_including_middlewares_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "99.9" + - record: job:django_http_requests_latency_seconds:quantile_rate30s + expr: histogram_quantile(0.50, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "50" + - record: job:django_http_requests_latency_seconds:quantile_rate30s + expr: histogram_quantile(0.95, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "95" + - record: job:django_http_requests_latency_seconds:quantile_rate30s + expr: histogram_quantile(0.99, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "99" + - record: job:django_http_requests_latency_seconds:quantile_rate30s + expr: histogram_quantile(0.999, sum(rate(django_http_requests_latency_seconds_bucket[30s])) by (job, le)) + labels: + quantile: "99.9" + - name: Aggregate model operations + rules: + - record: job:django_model_inserts_total:sum_rate1m + expr: sum(rate(django_model_inserts_total[1m])) by (job, model) + - record: job:django_model_updates_total:sum_rate1m + expr: sum(rate(django_model_updates_total[1m])) by (job, model) + - record: job:django_model_deletes_total:sum_rate1m + expr: sum(rate(django_model_deletes_total[1m])) by (job, model) + - name: Aggregate database operations + rules: + - record: job:django_db_new_connections_total:sum_rate30s + expr: sum(rate(django_db_new_connections_total[30s])) by (alias, vendor) + - record: job:django_db_new_connection_errors_total:sum_rate30s + expr: sum(rate(django_db_new_connection_errors_total[30s])) by (alias, vendor) + - record: job:django_db_execute_total:sum_rate30s + expr: sum(rate(django_db_execute_total[30s])) by (alias, vendor) + - record: job:django_db_execute_many_total:sum_rate30s + expr: sum(rate(django_db_execute_many_total[30s])) by (alias, vendor) + - record: job:django_db_errors_total:sum_rate30s + expr: sum(rate(django_db_errors_total[30s])) by (alias, vendor, type) + - name: Aggregate migrations + rules: + - record: job:django_migrations_applied_total:max + expr: max(django_migrations_applied_total) by (job, connection) + - record: job:django_migrations_unapplied_total:max + expr: max(django_migrations_unapplied_total) by (job, connection) + - name: Alerts + rules: + - alert: UnappliedMigrations + expr: job:django_migrations_unapplied_total:max > 0 + for: 1m + labels: + severity: testing +{{- end }} diff --git a/helm/templates/static-sm.yaml b/helm/templates/static-sm.yaml new file mode 100644 index 000000000..542e7ba41 --- /dev/null +++ b/helm/templates/static-sm.yaml @@ -0,0 +1,17 @@ +{{- if .Values.monitoring.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + helm.sh/chart: {{ include "authentik.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + name: {{ include "authentik.fullname" . }}-static-monitoring +spec: + endpoints: + - port: http + selector: + matchLabels: + k8s.goauthentik.io/component: static +{{- end }} diff --git a/helm/templates/web-deployment.yaml b/helm/templates/web-deployment.yaml index d68ced4d5..6b98ba27b 100644 --- a/helm/templates/web-deployment.yaml +++ b/helm/templates/web-deployment.yaml @@ -88,9 +88,17 @@ spec: secretKeyRef: name: "{{ .Release.Name }}-postgresql" key: "postgresql-password" + {{ if .Values.geoip.enabled -}} + - name: AUTHENTIK_AUTHENTIK__GEOIP + value: /geoip/GeoLite2-City.mmdb + {{- end }} volumeMounts: - name: authentik-uploads mountPath: /media + {{ if .Values.geoip.enabled -}} + - name: geoip + mountPath: /geoip + {{- end }} ports: - name: http containerPort: 8000 @@ -116,3 +124,8 @@ spec: - name: authentik-uploads persistentVolumeClaim: claimName: {{ include "authentik.fullname" . }}-uploads + {{ if .Values.geoip.enabled -}} + - name: geoip + persistentVolumeClaim: + claimName: {{ include "authentik.fullname" . }}-geoip + {{- end }} diff --git a/helm/templates/web-sm.yaml b/helm/templates/web-sm.yaml new file mode 100644 index 000000000..7c09a62fb --- /dev/null +++ b/helm/templates/web-sm.yaml @@ -0,0 +1,26 @@ +{{- if .Values.monitoring.enabled -}} +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + labels: + app.kubernetes.io/name: {{ include "authentik.name" . }} + helm.sh/chart: {{ include "authentik.chart" . }} + app.kubernetes.io/instance: {{ .Release.Name }} + app.kubernetes.io/managed-by: {{ .Release.Service }} + name: {{ include "authentik.fullname" . }}-web-monitoring +spec: + endpoints: + - basicAuth: + password: + name: {{ include "authentik.fullname" . }}-secret-key + key: SECRET_KEY + username: + name: {{ include "authentik.fullname" . }}-secret-key + key: monitoring_username + port: http + path: /metrics/ + interval: 10s + selector: + matchLabels: + k8s.goauthentik.io/component: web +{{- end }} diff --git a/helm/templates/worker-deployment.yaml b/helm/templates/worker-deployment.yaml index 60fb58f1f..7d62c2c6b 100644 --- a/helm/templates/worker-deployment.yaml +++ b/helm/templates/worker-deployment.yaml @@ -68,6 +68,15 @@ spec: secretKeyRef: name: "{{ .Release.Name }}-postgresql" key: "postgresql-password" + {{ if .Values.geoip.enabled -}} + - name: AUTHENTIK_AUTHENTIK__GEOIP + value: /geoip/GeoLite2-City.mmdb + {{- end }} + {{ if .Values.geoip.enabled -}} + volumeMounts: + - name: geoip + mountPath: /geoip + {{- end }} resources: requests: cpu: 150m @@ -75,3 +84,9 @@ spec: limits: cpu: 300m memory: 600M + {{ if .Values.geoip.enabled -}} + volumes: + - name: geoip + persistentVolumeClaim: + claimName: {{ include "authentik.fullname" . }}-geoip + {{- end -}} diff --git a/helm/values.test.yaml b/helm/values.test.yaml deleted file mode 100644 index 81fef9fde..000000000 --- a/helm/values.test.yaml +++ /dev/null @@ -1,22 +0,0 @@ -image: - tag: gh-master - pullPolicy: Always - -serverReplicas: 1 -workerReplicas: 1 - -config: - # Log level used by web and worker - # Can be either debug, info, warning, error - logLevel: debug - -ingress: - hosts: - - authentik.127.0.0.1.nip.io - -# These values influence the bundled postgresql and redis charts, but are also used by authentik to connect -postgresql: - postgresqlPassword: EK-5jnKfjrGRm<77 - -redis: - password: password diff --git a/helm/values.yaml b/helm/values.yaml index fca618b47..b2ee4f5be 100644 --- a/helm/values.yaml +++ b/helm/values.yaml @@ -14,6 +14,9 @@ workerReplicas: 1 # Enable the Kubernetes integration which lets authentik deploy outposts into kubernetes kubernetesIntegration: true +monitoring: + enabled: true + config: # Optionally specify fixed secret_key, otherwise generated automatically # secretKey: _k*@6h2u2@q-dku57hhgzb7tnx*ba9wodcb^s9g0j59@=y(@_o @@ -41,6 +44,13 @@ config: # Email address authentik will send from, should have a correct @domain from: authentik@localhost +# Enable MaxMind GeoIP +geoip: + enabled: false + accountId: "" + licenseKey: "" + image: maxmindinc/geoipupdate:latest + # Enable Database Backups to S3 # backup: # accessKey: access-key diff --git a/outpost/azure-pipelines.yml b/outpost/azure-pipelines.yml index 6dfb8a5d9..5e7a5d9b8 100644 --- a/outpost/azure-pipelines.yml +++ b/outpost/azure-pipelines.yml @@ -33,7 +33,7 @@ stages: - task: PublishPipelineArtifact@1 inputs: targetPath: 'outpost/pkg/' - artifact: 'swagger_client' + artifact: 'go_swagger_client' publishLocation: 'pipeline' - stage: lint jobs: @@ -51,7 +51,7 @@ stages: - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' - artifactName: 'swagger_client' + artifactName: 'go_swagger_client' path: "outpost/pkg/" - task: CmdLine@2 inputs: @@ -70,7 +70,7 @@ stages: - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' - artifactName: 'swagger_client' + artifactName: 'go_swagger_client' path: "outpost/pkg/" - task: Go@0 inputs: @@ -89,7 +89,7 @@ stages: - task: DownloadPipelineArtifact@2 inputs: buildType: 'current' - artifactName: 'swagger_client' + artifactName: 'go_swagger_client' path: "outpost/pkg/" - task: Bash@3 inputs: diff --git a/outpost/go.mod b/outpost/go.mod index 41b1916ef..326f58de1 100644 --- a/outpost/go.mod +++ b/outpost/go.mod @@ -24,7 +24,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/pquerna/cachecontrol v0.0.0-20200921180117-858c6e7e6b7e // indirect github.com/recws-org/recws v1.2.1 - github.com/sirupsen/logrus v1.8.0 + github.com/sirupsen/logrus v1.8.1 github.com/spf13/afero v1.5.1 // indirect github.com/spf13/cast v1.3.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect diff --git a/outpost/go.sum b/outpost/go.sum index b97c21f4f..64064bfc5 100644 --- a/outpost/go.sum +++ b/outpost/go.sum @@ -618,6 +618,8 @@ github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.8.0 h1:nfhvjKcUMhBMVqbKHJlk5RPrrfYr/NMo3692g0dwfWU= github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= diff --git a/outpost/proxy.Dockerfile b/outpost/proxy.Dockerfile index 6ef2cecf8..ad2660723 100644 --- a/outpost/proxy.Dockerfile +++ b/outpost/proxy.Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.16.0 AS builder +FROM golang:1.16.2 AS builder WORKDIR /work diff --git a/swagger.yaml b/swagger.yaml index a71fea5e9..5f7d6e9d7 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -29,10 +29,7 @@ paths: '200': description: Login Metrics per 1h schema: - description: '' - type: array - items: - $ref: '#/definitions/AdministrationMetrics' + $ref: '#/definitions/LoginMetrics' tags: - admin parameters: [] @@ -168,6 +165,711 @@ paths: tags: - admin parameters: [] + /authenticators/admin/static/: + get: + operationId: authenticators_admin_static_list + description: Viewset for static authenticator devices (for admins) + parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + parameters: [] + /authenticators/admin/static/{id}/: + get: + operationId: authenticators_admin_static_read + description: Viewset for static authenticator devices (for admins) + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + parameters: + - name: id + in: path + description: A unique integer value identifying this static device. + required: true + type: integer + /authenticators/admin/totp/: + get: + operationId: authenticators_admin_totp_list + description: Viewset for totp authenticator devices (for admins) + parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + parameters: [] + /authenticators/admin/totp/{id}/: + get: + operationId: authenticators_admin_totp_read + description: Viewset for totp authenticator devices (for admins) + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + parameters: + - name: id + in: path + description: A unique integer value identifying this TOTP device. + required: true + type: integer + /authenticators/admin/webauthn/: + get: + operationId: authenticators_admin_webauthn_list + description: Viewset for WebAuthn authenticator devices (for admins) + parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + parameters: [] + /authenticators/admin/webauthn/{id}/: + get: + operationId: authenticators_admin_webauthn_read + description: Viewset for WebAuthn authenticator devices (for admins) + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + parameters: + - name: id + in: path + description: A unique integer value identifying this WebAuthn Device. + required: true + type: integer + /authenticators/static/: + get: + operationId: authenticators_static_list + description: Viewset for static authenticator devices + parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + post: + operationId: authenticators_static_create + description: Viewset for static authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/StaticDevice' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + parameters: [] + /authenticators/static/{id}/: + get: + operationId: authenticators_static_read + description: Viewset for static authenticator devices + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + put: + operationId: authenticators_static_update + description: Viewset for static authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/StaticDevice' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + patch: + operationId: authenticators_static_partial_update + description: Viewset for static authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/StaticDevice' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/StaticDevice' + tags: + - authenticators + delete: + operationId: authenticators_static_delete + description: Viewset for static authenticator devices + parameters: [] + responses: + '204': + description: '' + tags: + - authenticators + parameters: + - name: id + in: path + description: A unique integer value identifying this static device. + required: true + type: integer + /authenticators/totp/: + get: + operationId: authenticators_totp_list + description: Viewset for totp authenticator devices + parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + post: + operationId: authenticators_totp_create + description: Viewset for totp authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/TOTPDevice' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + parameters: [] + /authenticators/totp/{id}/: + get: + operationId: authenticators_totp_read + description: Viewset for totp authenticator devices + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + put: + operationId: authenticators_totp_update + description: Viewset for totp authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/TOTPDevice' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + patch: + operationId: authenticators_totp_partial_update + description: Viewset for totp authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/TOTPDevice' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/TOTPDevice' + tags: + - authenticators + delete: + operationId: authenticators_totp_delete + description: Viewset for totp authenticator devices + parameters: [] + responses: + '204': + description: '' + tags: + - authenticators + parameters: + - name: id + in: path + description: A unique integer value identifying this TOTP device. + required: true + type: integer + /authenticators/webauthn/: + get: + operationId: authenticators_webauthn_list + description: Viewset for WebAuthn authenticator devices + parameters: + - name: name + in: query + description: '' + required: false + type: string + - name: ordering + in: query + description: Which field to use when ordering the results. + required: false + type: string + - name: search + in: query + description: A search term. + required: false + type: string + - name: page + in: query + description: Page Index + required: false + type: integer + - name: page_size + in: query + description: Page Size + required: false + type: integer + responses: + '200': + description: '' + schema: + required: + - results + - pagination + type: object + properties: + pagination: + required: + - next + - previous + - count + - current + - total_pages + - start_index + - end_index + type: object + properties: + next: + type: number + previous: + type: number + count: + type: number + current: + type: number + total_pages: + type: number + start_index: + type: number + end_index: + type: number + results: + type: array + items: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + post: + operationId: authenticators_webauthn_create + description: Viewset for WebAuthn authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/WebAuthnDevice' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + parameters: [] + /authenticators/webauthn/{id}/: + get: + operationId: authenticators_webauthn_read + description: Viewset for WebAuthn authenticator devices + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + put: + operationId: authenticators_webauthn_update + description: Viewset for WebAuthn authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/WebAuthnDevice' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + patch: + operationId: authenticators_webauthn_partial_update + description: Viewset for WebAuthn authenticator devices + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/WebAuthnDevice' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/WebAuthnDevice' + tags: + - authenticators + delete: + operationId: authenticators_webauthn_delete + description: Viewset for WebAuthn authenticator devices + parameters: [] + responses: + '204': + description: '' + tags: + - authenticators + parameters: + - name: id + in: path + description: A unique integer value identifying this WebAuthn Device. + required: true + type: integer /core/applications/: get: operationId: core_applications_list @@ -318,9 +1020,12 @@ paths: parameters: [] responses: '200': - description: '' + description: Coordinates for diagrams schema: - $ref: '#/definitions/Application' + description: '' + type: array + items: + $ref: '#/definitions/Coordinate' tags: - core parameters: @@ -1620,17 +2325,12 @@ paths: description: |- Send example notification using selected transport. Requires Modify permissions. - parameters: - - name: data - in: body - required: true - schema: - $ref: '#/definitions/NotificationTransport' + parameters: [] responses: - '201': - description: '' + '200': + description: Notification test serializer schema: - $ref: '#/definitions/NotificationTransport' + $ref: '#/definitions/NotificationTransportTest' tags: - events parameters: @@ -1822,6 +2522,42 @@ paths: required: true type: string format: uuid + /flows/executor/{flow_slug}/: + get: + operationId: flows_executor_get + description: Get the next pending challenge from the currently active flow. + parameters: [] + responses: + '200': + description: Challenge that gets sent to the client based on which stage + is currently active + schema: + $ref: '#/definitions/Challenge' + tags: + - flows + post: + operationId: flows_executor_solve + description: Solve the previously retrieved challenge and advanced to the next + stage. + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/ChallengeResponse' + responses: + '200': + description: Challenge that gets sent to the client based on which stage + is currently active + schema: + $ref: '#/definitions/Challenge' + tags: + - flows + parameters: + - name: flow_slug + in: path + required: true + type: string /flows/instances/: get: operationId: flows_instances_list @@ -9296,17 +10032,33 @@ paths: type: string format: uuid definitions: - AdministrationMetrics: + Coordinate: + description: Coordinates for diagrams + type: object + properties: + x_cord: + title: X cord + type: integer + readOnly: true + y_cord: + title: Y cord + type: integer + readOnly: true + LoginMetrics: description: Login Metrics per 1h type: object properties: logins_per_1h: - title: Logins per 1h - type: string + description: '' + type: array + items: + $ref: '#/definitions/Coordinate' readOnly: true logins_failed_per_1h: - title: Logins failed per 1h - type: string + description: '' + type: array + items: + $ref: '#/definitions/Coordinate' readOnly: true Task: description: Serialize TaskInfo and TaskResult @@ -9332,11 +10084,11 @@ definitions: format: date-time status: title: Status - type: integer + type: string enum: - - 1 - - 2 - - 4 + - SUCCESSFUL + - WARNING + - ERROR messages: description: '' type: array @@ -9358,6 +10110,107 @@ definitions: title: Outdated type: boolean readOnly: true + StaticDevice: + description: Serializer for static authenticator devices + required: + - name + type: object + properties: + name: + title: Name + description: The human-readable name of this device. + type: string + maxLength: 64 + minLength: 1 + token_set: + description: '' + type: array + items: + description: 'A single token belonging to a :class:`StaticDevice`. .. attribute:: + device *ForeignKey*: A foreign key to :class:`StaticDevice`. .. attribute:: + token *CharField*: A random string up to 16 characters.' + required: + - token + type: object + properties: + id: + title: ID + type: integer + readOnly: true + token: + title: Token + type: string + maxLength: 16 + minLength: 1 + device: + description: "A static :class:`~django_otp.models.Device` simply consists\ + \ of random tokens shared by the database and the user. These are\ + \ frequently used as emergency tokens in case a user's normal device\ + \ is lost or unavailable. They can be consumed in any order; each\ + \ token will be removed from the database as soon as it is used. This\ + \ model has no fields of its own, but serves as a container for :class:`StaticToken`\ + \ objects. .. attribute:: token_set The RelatedManager for our tokens." + required: + - name + - user + type: object + properties: + id: + title: ID + type: integer + readOnly: true + name: + title: Name + description: The human-readable name of this device. + type: string + maxLength: 64 + minLength: 1 + confirmed: + title: Confirmed + description: Is this device ready for use? + type: boolean + throttling_failure_timestamp: + title: Throttling failure timestamp + description: A timestamp of the last failed verification attempt. + Null if last attempt succeeded. + type: string + format: date-time + x-nullable: true + throttling_failure_count: + title: Throttling failure count + description: Number of successive failed attempts. + type: integer + maximum: 2147483647 + minimum: 0 + user: + title: User + description: The user that this device belongs to. + type: integer + readOnly: true + readOnly: true + TOTPDevice: + description: Serializer for totp authenticator devices + required: + - name + type: object + properties: + name: + title: Name + description: The human-readable name of this device. + type: string + maxLength: 64 + minLength: 1 + WebAuthnDevice: + description: Serializer for WebAuthn authenticator devices + required: + - name + type: object + properties: + name: + title: Name + type: string + maxLength: 200 + minLength: 1 Provider: title: Provider description: Provider Serializer @@ -9728,8 +10581,6 @@ definitions: type: integer Notification: description: Notification Serializer - required: - - event type: object properties: pk: @@ -9897,6 +10748,18 @@ definitions: webhook_url: title: Webhook url type: string + NotificationTransportTest: + description: Notification test serializer + required: + - messages + type: object + properties: + messages: + description: '' + type: array + items: + type: string + minLength: 1 Flow: description: Flow Serializer required: @@ -10050,6 +10913,55 @@ definitions: format: uuid readOnly: true uniqueItems: true + ErrorDetail: + description: Serializer for rest_framework's error messages + required: + - string + - code + type: object + properties: + string: + title: String + type: string + minLength: 1 + code: + title: Code + type: string + minLength: 1 + Challenge: + description: Challenge that gets sent to the client based on which stage is currently + active + required: + - type + type: object + properties: + type: + title: Type + type: string + enum: + - native + - shell + - redirect + component: + title: Component + type: string + minLength: 1 + title: + title: Title + type: string + minLength: 1 + response_errors: + title: Response errors + type: object + additionalProperties: + description: '' + type: array + items: + $ref: '#/definitions/ErrorDetail' + ChallengeResponse: + description: Base class for all challenge responses + type: object + properties: {} Cache: description: Generic cache stats for an object type: object @@ -10303,22 +11215,23 @@ definitions: readOnly: true TypeCreate: description: Types of an object that can be created + required: + - name + - description + - link type: object properties: name: title: Name type: string - readOnly: true minLength: 1 description: title: Description type: string - readOnly: true minLength: 1 link: title: Link type: string - readOnly: true minLength: 1 ServiceConnectionState: description: Serializer for Service connection state diff --git a/tests/e2e/test_provider_oauth2_oidc_implicit.py b/tests/e2e/test_provider_oauth2_oidc_implicit.py new file mode 100644 index 000000000..52bc5664b --- /dev/null +++ b/tests/e2e/test_provider_oauth2_oidc_implicit.py @@ -0,0 +1,273 @@ +"""test OAuth2 OpenID Provider flow""" +from json import loads +from sys import platform +from time import sleep +from unittest.case import skipUnless + +from docker import DockerClient, from_env +from docker.models.containers import Container +from docker.types import Healthcheck +from selenium.webdriver.common.by import By +from selenium.webdriver.support import expected_conditions as ec +from structlog.stdlib import get_logger + +from authentik.core.models import Application +from authentik.crypto.models import CertificateKeyPair +from authentik.flows.models import Flow +from authentik.policies.expression.models import ExpressionPolicy +from authentik.policies.models import PolicyBinding +from authentik.providers.oauth2.constants import ( + SCOPE_OPENID, + SCOPE_OPENID_EMAIL, + SCOPE_OPENID_PROFILE, +) +from authentik.providers.oauth2.generators import ( + generate_client_id, + generate_client_secret, +) +from authentik.providers.oauth2.models import ClientTypes, OAuth2Provider, ScopeMapping +from tests.e2e.utils import ( + USER, + SeleniumTestCase, + apply_migration, + object_manager, + retry, +) + +LOGGER = get_logger() + + +@skipUnless(platform.startswith("linux"), "requires local docker") +class TestProviderOAuth2OIDCImplicit(SeleniumTestCase): + """test OAuth with OpenID Provider flow""" + + def setUp(self): + self.client_id = generate_client_id() + self.client_secret = generate_client_secret() + self.application_slug = "test" + super().setUp() + + def setup_client(self) -> Container: + """Setup client saml-sp container which we test SAML against""" + sleep(1) + client: DockerClient = from_env() + container = client.containers.run( + image="beryju/oidc-test-client", + detach=True, + network_mode="host", + auto_remove=True, + healthcheck=Healthcheck( + test=["CMD", "wget", "--spider", "http://localhost:9009/health"], + interval=5 * 100 * 1000000, + start_period=1 * 100 * 1000000, + ), + environment={ + "OIDC_CLIENT_ID": self.client_id, + "OIDC_CLIENT_SECRET": self.client_secret, + "OIDC_PROVIDER": f"{self.live_server_url}/application/o/{self.application_slug}/", + }, + ) + while True: + container.reload() + status = container.attrs.get("State", {}).get("Health", {}).get("Status") + if status == "healthy": + return container + LOGGER.info("Container failed healthcheck") + sleep(1) + + @retry() + @apply_migration("authentik_core", "0003_default_user") + @apply_migration("authentik_flows", "0008_default_flows") + @apply_migration("authentik_flows", "0010_provider_flows") + @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + def test_redirect_uri_error(self): + """test OpenID Provider flow (invalid redirect URI, check error message)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + provider = OAuth2Provider.objects.create( + name=self.application_slug, + client_type=ClientTypes.CONFIDENTIAL, + client_id=self.client_id, + client_secret=self.client_secret, + rsa_key=CertificateKeyPair.objects.first(), + redirect_uris="http://localhost:9009/", + authorization_flow=authorization_flow, + ) + provider.property_mappings.set( + ScopeMapping.objects.filter( + scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] + ) + ) + provider.save() + Application.objects.create( + name=self.application_slug, + slug=self.application_slug, + provider=provider, + ) + self.container = self.setup_client() + + self.driver.get("http://localhost:9009/implicit/") + sleep(2) + self.assertEqual( + self.driver.find_element(By.CLASS_NAME, "pf-c-title").text, + "Redirect URI Error", + ) + + @retry() + @apply_migration("authentik_core", "0003_default_user") + @apply_migration("authentik_flows", "0008_default_flows") + @apply_migration("authentik_flows", "0010_provider_flows") + @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @object_manager + def test_authorization_consent_implied(self): + """test OpenID Provider flow (default authorization flow with implied consent)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-implicit-consent" + ) + provider = OAuth2Provider.objects.create( + name=self.application_slug, + client_type=ClientTypes.CONFIDENTIAL, + client_id=self.client_id, + client_secret=self.client_secret, + rsa_key=CertificateKeyPair.objects.first(), + redirect_uris="http://localhost:9009/implicit/", + authorization_flow=authorization_flow, + ) + provider.property_mappings.set( + ScopeMapping.objects.filter( + scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] + ) + ) + provider.save() + Application.objects.create( + name=self.application_slug, + slug=self.application_slug, + provider=provider, + ) + self.container = self.setup_client() + + self.driver.get("http://localhost:9009/implicit/") + self.login() + 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"], USER().username) + self.assertEqual(body["profile"]["name"], USER().name) + self.assertEqual(body["profile"]["email"], USER().email) + + @retry() + @apply_migration("authentik_core", "0003_default_user") + @apply_migration("authentik_flows", "0008_default_flows") + @apply_migration("authentik_flows", "0010_provider_flows") + @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + @object_manager + def test_authorization_consent_explicit(self): + """test OpenID Provider flow (default authorization flow with explicit consent)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-explicit-consent" + ) + provider = OAuth2Provider.objects.create( + name=self.application_slug, + authorization_flow=authorization_flow, + client_type=ClientTypes.CONFIDENTIAL, + client_id=self.client_id, + client_secret=self.client_secret, + rsa_key=CertificateKeyPair.objects.first(), + redirect_uris="http://localhost:9009/implicit/", + ) + provider.property_mappings.set( + ScopeMapping.objects.filter( + scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] + ) + ) + provider.save() + app = Application.objects.create( + name=self.application_slug, + slug=self.application_slug, + provider=provider, + ) + self.container = self.setup_client() + + self.driver.get("http://localhost:9009/implicit/") + self.login() + + self.wait.until( + ec.presence_of_element_located((By.CSS_SELECTOR, "ak-flow-executor")) + ) + + flow_executor = self.get_shadow_root("ak-flow-executor") + consent_stage = self.get_shadow_root("ak-stage-consent", flow_executor) + + self.assertIn( + app.name, + consent_stage.find_element(By.CSS_SELECTOR, "#header-text").text, + ) + consent_stage.find_element( + By.CSS_SELECTOR, + ("[type=submit]"), + ).click() + + 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) + + self.assertEqual(body["profile"]["nickname"], USER().username) + self.assertEqual(body["profile"]["name"], USER().name) + self.assertEqual(body["profile"]["email"], USER().email) + + @retry() + @apply_migration("authentik_core", "0003_default_user") + @apply_migration("authentik_flows", "0008_default_flows") + @apply_migration("authentik_flows", "0010_provider_flows") + @apply_migration("authentik_crypto", "0002_create_self_signed_kp") + def test_authorization_denied(self): + """test OpenID Provider flow (default authorization with access deny)""" + sleep(1) + # Bootstrap all needed objects + authorization_flow = Flow.objects.get( + slug="default-provider-authorization-explicit-consent" + ) + provider = OAuth2Provider.objects.create( + name=self.application_slug, + authorization_flow=authorization_flow, + client_type=ClientTypes.CONFIDENTIAL, + client_id=self.client_id, + client_secret=self.client_secret, + rsa_key=CertificateKeyPair.objects.first(), + redirect_uris="http://localhost:9009/implicit/", + ) + provider.property_mappings.set( + ScopeMapping.objects.filter( + scope_name__in=[SCOPE_OPENID, SCOPE_OPENID_EMAIL, SCOPE_OPENID_PROFILE] + ) + ) + provider.save() + app = Application.objects.create( + name=self.application_slug, + slug=self.application_slug, + provider=provider, + ) + + negative_policy = ExpressionPolicy.objects.create( + name="negative-static", expression="return False" + ) + PolicyBinding.objects.create(target=app, policy=negative_policy, order=0) + + self.container = self.setup_client() + self.driver.get("http://localhost:9009/implicit/") + self.login() + self.wait.until( + ec.presence_of_element_located((By.CSS_SELECTOR, "header > h1")) + ) + self.assertEqual( + self.driver.find_element(By.CSS_SELECTOR, "header > h1").text, + "Permission denied", + ) diff --git a/tests/integration/test_outpost_docker.py b/tests/integration/test_outpost_docker.py index 9b491c5af..8a1c4dc69 100644 --- a/tests/integration/test_outpost_docker.py +++ b/tests/integration/test_outpost_docker.py @@ -3,11 +3,13 @@ from shutil import rmtree from tempfile import mkdtemp from time import sleep +import yaml from django.test import TestCase from docker import DockerClient, from_env from docker.models.containers import Container from docker.types.healthcheck import Healthcheck +from authentik import __version__ from authentik.crypto.models import CertificateKeyPair from authentik.flows.models import Flow from authentik.outposts.apps import AuthentikOutpostConfig @@ -93,3 +95,14 @@ class OutpostDockerTests(TestCase): controller = DockerController(self.outpost, self.service_connection) controller.up() controller.down() + + def test_docker_static(self): + """test that deployment requires update""" + controller = DockerController(self.outpost, self.service_connection) + manifest = controller.get_static_deployment() + compose = yaml.load(manifest, Loader=yaml.SafeLoader) + self.assertEqual(compose["version"], "3.5") + self.assertEqual( + compose["services"]["authentik_proxy"]["image"], + f"beryju/authentik-proxy:{__version__}", + ) diff --git a/tests/integration/test_outposts_kubernetes.py b/tests/integration/test_outpost_kubernetes.py similarity index 100% rename from tests/integration/test_outposts_kubernetes.py rename to tests/integration/test_outpost_kubernetes.py diff --git a/tests/integration/test_proxy_docker.py b/tests/integration/test_proxy_docker.py new file mode 100644 index 000000000..532810230 --- /dev/null +++ b/tests/integration/test_proxy_docker.py @@ -0,0 +1,108 @@ +"""outpost tests""" +from shutil import rmtree +from tempfile import mkdtemp +from time import sleep + +import yaml +from django.test import TestCase +from docker import DockerClient, from_env +from docker.models.containers import Container +from docker.types.healthcheck import Healthcheck + +from authentik import __version__ +from authentik.crypto.models import CertificateKeyPair +from authentik.flows.models import Flow +from authentik.outposts.apps import AuthentikOutpostConfig +from authentik.outposts.models import DockerServiceConnection, Outpost, OutpostType +from authentik.providers.proxy.controllers.docker import DockerController +from authentik.providers.proxy.models import ProxyProvider + + +class TestProxyDocker(TestCase): + """Test Docker Controllers""" + + def _start_container(self, ssl_folder: str) -> Container: + client: DockerClient = from_env() + container = client.containers.run( + image="library/docker:dind", + detach=True, + network_mode="host", + remove=True, + privileged=True, + healthcheck=Healthcheck( + test=["CMD", "docker", "info"], + interval=5 * 100 * 1000000, + start_period=5 * 100 * 1000000, + ), + environment={"DOCKER_TLS_CERTDIR": "/ssl"}, + volumes={ + f"{ssl_folder}/": { + "bind": "/ssl", + } + }, + ) + while True: + container.reload() + status = container.attrs.get("State", {}).get("Health", {}).get("Status") + if status == "healthy": + return container + sleep(1) + + def setUp(self): + super().setUp() + self.ssl_folder = mkdtemp() + self.container = self._start_container(self.ssl_folder) + # Ensure that local connection have been created + AuthentikOutpostConfig.init_local_connection() + self.provider: ProxyProvider = ProxyProvider.objects.create( + name="test", + internal_host="http://localhost", + external_host="http://localhost", + authorization_flow=Flow.objects.first(), + ) + authentication_kp = CertificateKeyPair.objects.create( + name="docker-authentication", + certificate_data=open(f"{self.ssl_folder}/client/cert.pem").read(), + key_data=open(f"{self.ssl_folder}/client/key.pem").read(), + ) + verification_kp = CertificateKeyPair.objects.create( + name="docker-verification", + certificate_data=open(f"{self.ssl_folder}/client/ca.pem").read(), + ) + self.service_connection = DockerServiceConnection.objects.create( + url="https://localhost:2376", + tls_verification=verification_kp, + tls_authentication=authentication_kp, + ) + self.outpost: Outpost = Outpost.objects.create( + name="test", + type=OutpostType.PROXY, + service_connection=self.service_connection, + ) + self.outpost.providers.add(self.provider) + self.outpost.save() + + def tearDown(self) -> None: + super().tearDown() + self.container.kill() + try: + rmtree(self.ssl_folder) + except PermissionError: + pass + + def test_docker_controller(self): + """test that deployment requires update""" + controller = DockerController(self.outpost, self.service_connection) + controller.up() + controller.down() + + def test_docker_static(self): + """test that deployment requires update""" + controller = DockerController(self.outpost, self.service_connection) + manifest = controller.get_static_deployment() + compose = yaml.load(manifest, Loader=yaml.SafeLoader) + self.assertEqual(compose["version"], "3.5") + self.assertEqual( + compose["services"]["authentik_proxy"]["image"], + f"beryju/authentik-proxy:{__version__}", + ) diff --git a/tests/integration/test_proxy_kubernetes.py b/tests/integration/test_proxy_kubernetes.py index 89f83f5d7..61b9a92aa 100644 --- a/tests/integration/test_proxy_kubernetes.py +++ b/tests/integration/test_proxy_kubernetes.py @@ -9,7 +9,7 @@ from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesCont from authentik.providers.proxy.models import ProxyProvider -class TestControllers(TestCase): +class TestProxyKubernetes(TestCase): """Test Controllers""" def setUp(self): diff --git a/web/.eslintignore b/web/.eslintignore index 537b7fae4..0799251cf 100644 --- a/web/.eslintignore +++ b/web/.eslintignore @@ -4,3 +4,8 @@ node_modules dist # don't lint nyc coverage output coverage +# don't lint generated code +src/api/apis +src/api/models +src/api/index.ts +src/api/runtime.ts diff --git a/web/azure-pipelines.yml b/web/azure-pipelines.yml index b09e9fe58..5275b7e3d 100644 --- a/web/azure-pipelines.yml +++ b/web/azure-pipelines.yml @@ -10,6 +10,25 @@ variables: branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }} stages: + - stage: generate + jobs: + - job: swagger_generate + pool: + vmImage: 'ubuntu-latest' + steps: + - task: NodeTool@0 + inputs: + versionSpec: '12.x' + displayName: 'Install Node.js' + - task: CmdLine@2 + inputs: + script: | + docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true + - task: PublishPipelineArtifact@1 + inputs: + targetPath: 'web/src/api/' + artifact: 'ts_swagger_client' + publishLocation: 'pipeline' - stage: lint jobs: - job: eslint @@ -20,6 +39,11 @@ stages: inputs: versionSpec: '12.x' displayName: 'Install Node.js' + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'ts_swagger_client' + path: "web/src/api/" - task: Npm@1 inputs: command: 'install' @@ -37,6 +61,11 @@ stages: inputs: versionSpec: '12.x' displayName: 'Install Node.js' + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'ts_swagger_client' + path: "web/src/api/" - task: Npm@1 inputs: command: 'install' @@ -56,6 +85,11 @@ stages: inputs: versionSpec: '12.x' displayName: 'Install Node.js' + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'ts_swagger_client' + path: "web/src/api/" - task: Npm@1 inputs: command: 'install' @@ -71,16 +105,21 @@ stages: pool: vmImage: 'ubuntu-latest' steps: - - task: Bash@3 - inputs: - targetType: 'inline' - script: | - python ./scripts/az_do_set_branch.py - - task: Docker@2 - inputs: - containerRegistry: 'beryjuorg-harbor' - repository: 'authentik/static' - command: 'buildAndPush' - Dockerfile: 'web/Dockerfile' - tags: "gh-$(branchName)" - buildContext: 'web/' + - task: DownloadPipelineArtifact@2 + inputs: + buildType: 'current' + artifactName: 'ts_swagger_client' + path: "web/src/api/" + - task: Bash@3 + inputs: + targetType: 'inline' + script: | + python ./scripts/az_do_set_branch.py + - task: Docker@2 + inputs: + containerRegistry: 'beryjuorg-harbor' + repository: 'authentik/static' + command: 'buildAndPush' + Dockerfile: 'web/Dockerfile' + tags: "gh-$(branchName)" + buildContext: 'web/' diff --git a/web/package-lock.json b/web/package-lock.json index f0a718d0a..082c9cdfd 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -103,9 +103,9 @@ } }, "@patternfly/patternfly": { - "version": "4.87.3", - "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.87.3.tgz", - "integrity": "sha512-hDNMPa7B1zKD8LWFZO4SS5hC/N+yvuci2sAn8HJd+EIbAvbMAUkRsyZ0/XO3BG3RVtpSlgq7q8x1pAHC/FTFuA==" + "version": "4.90.5", + "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-4.90.5.tgz", + "integrity": "sha512-Fe0C8UkzSjtacQ+fHXlFB/LHzrv/c2K4z479C6dboOgkGQE1FyB0wt1NBfxij0D++rhOy04OOYdE+Tr0JSlZKw==" }, "@rollup/plugin-typescript": { "version": "8.2.0", @@ -143,27 +143,27 @@ } }, "@sentry/browser": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.2.1.tgz", - "integrity": "sha512-OAikFZ9EimD3noxMp8tA6Cf6qJcQ2U8k5QSgTPwdx+09nZOGJzbRFteK7WWmrS93ZJdzN61lpSQbg5v+bmmfbQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.2.2.tgz", + "integrity": "sha512-K5UGyEePtVPZIFMoiRafhd4Ov0M1kdozVsVKIPZrOpJyjQdPNX+fYDNL/h0nVmgOlE2S/uu4fl4mEfe/6aLShw==", "requires": { - "@sentry/core": "6.2.1", - "@sentry/types": "6.2.1", - "@sentry/utils": "6.2.1", + "@sentry/core": "6.2.2", + "@sentry/types": "6.2.2", + "@sentry/utils": "6.2.2", "tslib": "^1.9.3" }, "dependencies": { "@sentry/types": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.1.tgz", - "integrity": "sha512-h0OV1QT+fv5ojfK5/+iEXClu33HirmvbjcQC2jf05IHj9yXIOWy6EB10S8nBjuLiiFqQiAQYj3FN9Ip4eN8NJA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.2.tgz", + "integrity": "sha512-Y/1sRtw3a5JU4YdNBig8lLSVJ1UdYtuge+QP1CVLcLSAbq07Ok1bvF+Z+BlNcnHqle2Fl8aKuryG5Yu86enOyQ==" }, "@sentry/utils": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.1.tgz", - "integrity": "sha512-6kQgM/yBPdXu+3qbJnI6HBcWztN9QfiMkH++ZiKk4ERhg9d2LYWlze478uTU5Fyo/JQYcp+McpjtjpR9QIrr0g==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.2.tgz", + "integrity": "sha512-qaee6X6VDNZ8HeO83/veaKw0KuhDE7j1R+Yryme3PywFzsoTzutDrEQjb7gvcHAhBaAYX8IHUBHgxcFI9BxI+w==", "requires": { - "@sentry/types": "6.2.1", + "@sentry/types": "6.2.2", "tslib": "^1.9.3" } }, @@ -175,48 +175,48 @@ } }, "@sentry/core": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.2.1.tgz", - "integrity": "sha512-jPqQEtafxxDtLONhCbTHh/Uq8mZRhsfbwJTSVYfPVEe/ELfFZLQK7tP6rOh7zEWKbTkE0mE6XcaoH3ZRAhgrqg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.2.2.tgz", + "integrity": "sha512-qqWbvvXtymfXh7N5eEvk97MCnMURuyFIgqWdVD4MQM6yIfDCy36CyGfuQ3ViHTLZGdIfEOhLL9/f4kzf1RzqBA==", "requires": { - "@sentry/hub": "6.2.1", - "@sentry/minimal": "6.2.1", - "@sentry/types": "6.2.1", - "@sentry/utils": "6.2.1", + "@sentry/hub": "6.2.2", + "@sentry/minimal": "6.2.2", + "@sentry/types": "6.2.2", + "@sentry/utils": "6.2.2", "tslib": "^1.9.3" }, "dependencies": { "@sentry/hub": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.2.1.tgz", - "integrity": "sha512-pG7wCQeRpzeP6t0bT4T0X029R19dbDS3/qswF8BL6bg0AI3afjfjBAZm/fqn1Uwe/uBoMHVVdbxgJDZeQ5d4rQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.2.2.tgz", + "integrity": "sha512-VR6uQGRYt6RP633FHShlSLj0LUKGVrlTeSlwCoooWM5FR9lmi6akAaweuxpG78/kZvXrAWpjX6/nuYwHKGwzGA==", "requires": { - "@sentry/types": "6.2.1", - "@sentry/utils": "6.2.1", + "@sentry/types": "6.2.2", + "@sentry/utils": "6.2.2", "tslib": "^1.9.3" } }, "@sentry/minimal": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.2.1.tgz", - "integrity": "sha512-wuSXB4Ayxv9rBEQ4pm7fnG4UU2ZPtPnnChoEfd4/mw1UthXSvmPFEn6O4pdo2G8fTkl8eqm6wT/Q7uIXMEmw+A==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.2.2.tgz", + "integrity": "sha512-l0IgoGQgg1lTd4qDU8bQn25sbZBg8PwIHfuTLbGMlRr1flDXHOM1UXajWK/UKbAPelnU7M2JBSVzgl7PwjprzA==", "requires": { - "@sentry/hub": "6.2.1", - "@sentry/types": "6.2.1", + "@sentry/hub": "6.2.2", + "@sentry/types": "6.2.2", "tslib": "^1.9.3" } }, "@sentry/types": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.1.tgz", - "integrity": "sha512-h0OV1QT+fv5ojfK5/+iEXClu33HirmvbjcQC2jf05IHj9yXIOWy6EB10S8nBjuLiiFqQiAQYj3FN9Ip4eN8NJA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.2.tgz", + "integrity": "sha512-Y/1sRtw3a5JU4YdNBig8lLSVJ1UdYtuge+QP1CVLcLSAbq07Ok1bvF+Z+BlNcnHqle2Fl8aKuryG5Yu86enOyQ==" }, "@sentry/utils": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.1.tgz", - "integrity": "sha512-6kQgM/yBPdXu+3qbJnI6HBcWztN9QfiMkH++ZiKk4ERhg9d2LYWlze478uTU5Fyo/JQYcp+McpjtjpR9QIrr0g==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.2.tgz", + "integrity": "sha512-qaee6X6VDNZ8HeO83/veaKw0KuhDE7j1R+Yryme3PywFzsoTzutDrEQjb7gvcHAhBaAYX8IHUBHgxcFI9BxI+w==", "requires": { - "@sentry/types": "6.2.1", + "@sentry/types": "6.2.2", "tslib": "^1.9.3" } }, @@ -228,12 +228,12 @@ } }, "@sentry/hub": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.2.1.tgz", - "integrity": "sha512-pG7wCQeRpzeP6t0bT4T0X029R19dbDS3/qswF8BL6bg0AI3afjfjBAZm/fqn1Uwe/uBoMHVVdbxgJDZeQ5d4rQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.2.2.tgz", + "integrity": "sha512-VR6uQGRYt6RP633FHShlSLj0LUKGVrlTeSlwCoooWM5FR9lmi6akAaweuxpG78/kZvXrAWpjX6/nuYwHKGwzGA==", "requires": { - "@sentry/types": "6.2.1", - "@sentry/utils": "6.2.1", + "@sentry/types": "6.2.2", + "@sentry/utils": "6.2.2", "tslib": "^1.9.3" }, "dependencies": { @@ -245,12 +245,12 @@ } }, "@sentry/minimal": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.2.1.tgz", - "integrity": "sha512-wuSXB4Ayxv9rBEQ4pm7fnG4UU2ZPtPnnChoEfd4/mw1UthXSvmPFEn6O4pdo2G8fTkl8eqm6wT/Q7uIXMEmw+A==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.2.2.tgz", + "integrity": "sha512-l0IgoGQgg1lTd4qDU8bQn25sbZBg8PwIHfuTLbGMlRr1flDXHOM1UXajWK/UKbAPelnU7M2JBSVzgl7PwjprzA==", "requires": { - "@sentry/hub": "6.2.1", - "@sentry/types": "6.2.1", + "@sentry/hub": "6.2.2", + "@sentry/types": "6.2.2", "tslib": "^1.9.3" }, "dependencies": { @@ -262,14 +262,14 @@ } }, "@sentry/tracing": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.2.1.tgz", - "integrity": "sha512-bvStY1SnL08wkSeVK3j9K5rivQQJdKFCPR2VYRFOCaUoleZ6ChPUnBvxQ/E2LXc0hk/y/wo1q4r5B0dfCCY+bQ==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.2.2.tgz", + "integrity": "sha512-mAkPoqtofNfka/u9rOVVDQPaEoTmr0AQh654g9ZqsaqsOJLKjB4FDLVNubWs90fjeKqHiYkI3ZHPak2TzHBPkw==", "requires": { - "@sentry/hub": "6.2.1", - "@sentry/minimal": "6.2.1", - "@sentry/types": "6.2.1", - "@sentry/utils": "6.2.1", + "@sentry/hub": "6.2.2", + "@sentry/minimal": "6.2.2", + "@sentry/types": "6.2.2", + "@sentry/utils": "6.2.2", "tslib": "^1.9.3" }, "dependencies": { @@ -281,16 +281,16 @@ } }, "@sentry/types": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.1.tgz", - "integrity": "sha512-h0OV1QT+fv5ojfK5/+iEXClu33HirmvbjcQC2jf05IHj9yXIOWy6EB10S8nBjuLiiFqQiAQYj3FN9Ip4eN8NJA==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.2.2.tgz", + "integrity": "sha512-Y/1sRtw3a5JU4YdNBig8lLSVJ1UdYtuge+QP1CVLcLSAbq07Ok1bvF+Z+BlNcnHqle2Fl8aKuryG5Yu86enOyQ==" }, "@sentry/utils": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.1.tgz", - "integrity": "sha512-6kQgM/yBPdXu+3qbJnI6HBcWztN9QfiMkH++ZiKk4ERhg9d2LYWlze478uTU5Fyo/JQYcp+McpjtjpR9QIrr0g==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.2.2.tgz", + "integrity": "sha512-qaee6X6VDNZ8HeO83/veaKw0KuhDE7j1R+Yryme3PywFzsoTzutDrEQjb7gvcHAhBaAYX8IHUBHgxcFI9BxI+w==", "requires": { - "@sentry/types": "6.2.1", + "@sentry/types": "6.2.2", "tslib": "^1.9.3" }, "dependencies": { @@ -310,12 +310,13 @@ } }, "@types/clean-css": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.2.tgz", - "integrity": "sha512-xiTJn3bmDh1lA8c6iVJs4ZhHw+pcmxXlJQXOB6G1oULaak8rmarIeFKI4aTJ7849dEhaO612wgIualZfbxTJwA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.3.tgz", + "integrity": "sha512-ET0ldU/vpXecy5vO8JRIhtJWSrk1vzXdJcp3Bjf8bARZynl6vfkhEKY/A7njfNIRlmyTGuVFuqnD6I3tOGdXpQ==", "dev": true, "requires": { - "@types/node": "*" + "@types/node": "*", + "source-map": "^0.6.0" } }, "@types/codemirror": { @@ -404,22 +405,22 @@ } }, "@types/uglify-js": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.11.0.tgz", - "integrity": "sha512-I0Yd8TUELTbgRHq2K65j8rnDPAzAP+DiaF/syLem7yXwYLsHZhPd+AM2iXsWmf9P2F2NlFCgl5erZPQx9IbM9Q==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.0.tgz", + "integrity": "sha512-EGkrJD5Uy+Pg0NUR8uA4bJ5WMfljyad0G+784vLCNUkD+QwOJXUbBYExXfVGf7YtyzdQp3L/XMYcliB987kL5Q==", "dev": true, "requires": { "source-map": "^0.6.1" } }, "@typescript-eslint/eslint-plugin": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz", - "integrity": "sha512-SK777klBdlkUZpZLC1mPvyOWk9yAFCWmug13eAjVQ4/Q1LATE/NbcQL1xDHkptQkZOLnPmLUA1Y54m8dqYwnoQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz", + "integrity": "sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw==", "dev": true, "requires": { - "@typescript-eslint/experimental-utils": "4.16.1", - "@typescript-eslint/scope-manager": "4.16.1", + "@typescript-eslint/experimental-utils": "4.17.0", + "@typescript-eslint/scope-manager": "4.17.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "lodash": "^4.17.15", @@ -429,112 +430,55 @@ } }, "@typescript-eslint/experimental-utils": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.16.1.tgz", - "integrity": "sha512-0Hm3LSlMYFK17jO4iY3un1Ve9x1zLNn4EM50Lia+0EV99NdbK+cn0er7HC7IvBA23mBg3P+8dUkMXy4leL33UQ==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz", + "integrity": "sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA==", "dev": true, "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.16.1", - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/typescript-estree": "4.16.1", + "@typescript-eslint/scope-manager": "4.17.0", + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/typescript-estree": "4.17.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" } }, "@typescript-eslint/parser": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.16.1.tgz", - "integrity": "sha512-/c0LEZcDL5y8RyI1zLcmZMvJrsR6SM1uetskFkoh3dvqDKVXPsXI+wFB/CbVw7WkEyyTKobC1mUNp/5y6gRvXg==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.17.0.tgz", + "integrity": "sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "4.16.1", - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/typescript-estree": "4.16.1", + "@typescript-eslint/scope-manager": "4.17.0", + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/typescript-estree": "4.17.0", "debug": "^4.1.1" - }, - "dependencies": { - "@typescript-eslint/scope-manager": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz", - "integrity": "sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/visitor-keys": "4.16.1" - } - }, - "@typescript-eslint/types": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.16.1.tgz", - "integrity": "sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz", - "integrity": "sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/visitor-keys": "4.16.1", - "debug": "^4.1.1", - "globby": "^11.0.1", - "is-glob": "^4.0.1", - "semver": "^7.3.2", - "tsutils": "^3.17.1" - } - }, - "@typescript-eslint/visitor-keys": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz", - "integrity": "sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w==", - "dev": true, - "requires": { - "@typescript-eslint/types": "4.16.1", - "eslint-visitor-keys": "^2.0.0" - } - }, - "globby": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", - "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - } } }, "@typescript-eslint/scope-manager": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.16.1.tgz", - "integrity": "sha512-6IlZv9JaurqV0jkEg923cV49aAn8V6+1H1DRfhRcvZUrptQ+UtSKHb5kwTayzOYTJJ/RsYZdcvhOEKiBLyc0Cw==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz", + "integrity": "sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==", "dev": true, "requires": { - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/visitor-keys": "4.16.1" + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/visitor-keys": "4.17.0" } }, "@typescript-eslint/types": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.16.1.tgz", - "integrity": "sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.17.0.tgz", + "integrity": "sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.16.1.tgz", - "integrity": "sha512-m8I/DKHa8YbeHt31T+UGd/l8Kwr0XCTCZL3H4HMvvLCT7HU9V7yYdinTOv1gf/zfqNeDcCgaFH2BMsS8x6NvJg==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz", + "integrity": "sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.16.1", - "@typescript-eslint/visitor-keys": "4.16.1", + "@typescript-eslint/types": "4.17.0", + "@typescript-eslint/visitor-keys": "4.17.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -559,12 +503,12 @@ } }, "@typescript-eslint/visitor-keys": { - "version": "4.16.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.16.1.tgz", - "integrity": "sha512-s/aIP1XcMkEqCNcPQtl60ogUYjSM8FU2mq1O7y5cFf3Xcob1z1iXWNB6cC43Op+NGRTFgGolri6s8z/efA9i1w==", + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz", + "integrity": "sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==", "dev": true, "requires": { - "@typescript-eslint/types": "4.16.1", + "@typescript-eslint/types": "4.17.0", "eslint-visitor-keys": "^2.0.0" } }, @@ -2280,16 +2224,16 @@ } }, "minify-html-literals": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/minify-html-literals/-/minify-html-literals-1.3.2.tgz", - "integrity": "sha512-DBdi0md84vjvwmLoo9xleFV5FkhzOwfKBqcmoVFL54c9CFlSBtG9KTKEQqiwscB+acewculqys1cDnwyrYlNtg==", + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/minify-html-literals/-/minify-html-literals-1.3.5.tgz", + "integrity": "sha512-p8T8ryePRR8FVfJZLVFmM53WY25FL0moCCTycUDuAu6rf9GMLwy0gNjXBGNin3Yun7Y+tIWd28axOf0t2EpAlQ==", "dev": true, "requires": { "@types/html-minifier": "^3.5.3", "clean-css": "^4.2.1", "html-minifier": "^4.0.0", "magic-string": "^0.25.0", - "parse-literals": "^1.2.0" + "parse-literals": "^1.2.1" } }, "minimatch": { @@ -2489,20 +2433,12 @@ } }, "parse-literals": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parse-literals/-/parse-literals-1.2.0.tgz", - "integrity": "sha512-gh4zPwvFSXx9ginX8lu9MP3OPHN3VV12PXI8IXD6oMCklFqM82pfbU9e/PKf9r7oLpbqlDSDyHYSVlxxuq3Iew==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/parse-literals/-/parse-literals-1.2.1.tgz", + "integrity": "sha512-Ml0w104Ph2wwzuRdxrg9booVWsngXbB4bZ5T2z6WyF8b5oaNkUmBiDtahi34yUIpXD8Y13JjAK6UyIyApJ73RQ==", "dev": true, "requires": { - "typescript": "^2.9.2 || ^3.0.0" - }, - "dependencies": { - "typescript": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", - "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", - "dev": true - } + "typescript": "^2.9.2 || ^3.0.0 || ^4.0.0" } }, "parse5": { @@ -2717,9 +2653,9 @@ } }, "rollup": { - "version": "2.40.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.40.0.tgz", - "integrity": "sha512-WiOGAPbXoHu+TOz6hyYUxIksOwsY/21TRWoO593jgYt8mvYafYqQl+axaA8y1z2HFazNUUrsMSjahV2A6/2R9A==", + "version": "2.41.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.41.1.tgz", + "integrity": "sha512-nepLFAW5W71/MWpS2Yr7r31eS7HRfYg2RXnxb6ehqN9zY42yACxKtEfb4xq8SmNfUohAzGMcyl6jkwdLOAiUbg==", "requires": { "fsevents": "~2.3.1" } @@ -2801,12 +2737,12 @@ } }, "rollup-plugin-minify-html-literals": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/rollup-plugin-minify-html-literals/-/rollup-plugin-minify-html-literals-1.2.5.tgz", - "integrity": "sha512-x4FzCnbBpYdme7MQDS3+18CvYLqakAtM/JmA3hqXplwzMeZWW3l14KU7H33RhJlHH8Klgv49hGtBRLWLfjCudw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rollup-plugin-minify-html-literals/-/rollup-plugin-minify-html-literals-1.2.6.tgz", + "integrity": "sha512-JRq2fjlCTiw0zu+1Sy3ClHGCxA79dWGr4HLHWSQgd060StVW9fBVksuj8Xw/suPkNSGClJf/4xNQ1MF6JeXPaw==", "dev": true, "requires": { - "minify-html-literals": "^1.3.2", + "minify-html-literals": "^1.3.5", "rollup-pluginutils": "^2.8.2" } }, @@ -3383,15 +3319,15 @@ "dev": true }, "typescript": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.2.tgz", - "integrity": "sha512-tbb+NVrLfnsJy3M59lsDgrzWIflR4d4TIUjz+heUnHZwdF7YsrMTKoRERiIvI2lvBG95dfpLxB21WZhys1bgaQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", "dev": true }, "uglify-js": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.11.0.tgz", - "integrity": "sha512-e1KQFRCpOxnrJsJVqDUCjURq+wXvIn7cK2sRAx9XL3HYLL9aezOP4Pb1+Y3/o693EPk111Yj2Q+IUXxcpHlygQ==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.0.tgz", + "integrity": "sha512-TWYSWa9T2pPN4DIJYbU9oAjQx+5qdV5RUDxwARg8fmJZrD/V27Zj0JngW5xg1DFz42G0uDYl2XhzF6alSzD62w==", "dev": true }, "union-value": { diff --git a/web/package.json b/web/package.json index 29d2f8db8..e8189514a 100644 --- a/web/package.json +++ b/web/package.json @@ -11,9 +11,9 @@ }, "dependencies": { "@fortawesome/fontawesome-free": "^5.15.2", - "@patternfly/patternfly": "^4.87.3", - "@sentry/browser": "^6.2.1", - "@sentry/tracing": "^6.2.1", + "@patternfly/patternfly": "^4.90.5", + "@sentry/browser": "^6.2.2", + "@sentry/tracing": "^6.2.2", "@types/chart.js": "^2.9.31", "@types/codemirror": "0.0.108", "@types/grecaptcha": "^3.0.1", @@ -24,7 +24,7 @@ "flowchart.js": "^1.15.0", "lit-element": "^2.4.0", "lit-html": "^1.3.0", - "rollup": "^2.40.0", + "rollup": "^2.41.1", "rollup-plugin-copy": "^3.4.0", "rollup-plugin-cssimport": "^1.0.2", "rollup-plugin-external-globals": "^0.6.1", @@ -33,17 +33,17 @@ }, "devDependencies": { "@rollup/plugin-typescript": "^8.2.0", - "@typescript-eslint/eslint-plugin": "^4.16.1", - "@typescript-eslint/parser": "^4.16.1", + "@typescript-eslint/eslint-plugin": "^4.17.0", + "@typescript-eslint/parser": "^4.17.0", "eslint": "^7.21.0", "eslint-config-google": "^0.14.0", "eslint-plugin-lit": "^1.3.0", "rollup-plugin-commonjs": "^10.1.0", - "rollup-plugin-minify-html-literals": "^1.2.5", + "rollup-plugin-minify-html-literals": "^1.2.6", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-sourcemaps": "^0.6.3", "rollup-plugin-terser": "^7.0.2", "ts-lit-plugin": "^1.2.1", - "typescript": "^4.2.2" + "typescript": "^4.2.3" } } diff --git a/web/src/api/.gitignore b/web/src/api/.gitignore new file mode 100644 index 000000000..d973fa4fa --- /dev/null +++ b/web/src/api/.gitignore @@ -0,0 +1,4 @@ +apis/** +models/** +index.ts +runtime.ts diff --git a/web/src/api/.openapi-generator-ignore b/web/src/api/.openapi-generator-ignore new file mode 100644 index 000000000..7484ee590 --- /dev/null +++ b/web/src/api/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/web/src/api/.openapi-generator/FILES b/web/src/api/.openapi-generator/FILES new file mode 100644 index 000000000..e6cc1a8c5 --- /dev/null +++ b/web/src/api/.openapi-generator/FILES @@ -0,0 +1,167 @@ +apis/AdminApi.ts +apis/CoreApi.ts +apis/CryptoApi.ts +apis/EventsApi.ts +apis/FlowsApi.ts +apis/OutpostsApi.ts +apis/PoliciesApi.ts +apis/PropertymappingsApi.ts +apis/ProvidersApi.ts +apis/RootApi.ts +apis/SourcesApi.ts +apis/StagesApi.ts +apis/index.ts +index.ts +models/Application.ts +models/AuthenticateWebAuthnStage.ts +models/AuthenticatorStaticStage.ts +models/AuthenticatorTOTPStage.ts +models/AuthenticatorValidateStage.ts +models/Cache.ts +models/CaptchaStage.ts +models/CertificateData.ts +models/CertificateKeyPair.ts +models/Challenge.ts +models/Config.ts +models/ConsentStage.ts +models/Coordinate.ts +models/DenyStage.ts +models/DockerServiceConnection.ts +models/DummyPolicy.ts +models/DummyStage.ts +models/EmailStage.ts +models/ErrorDetail.ts +models/Event.ts +models/EventMatcherPolicy.ts +models/EventTopPerUser.ts +models/ExpressionPolicy.ts +models/Flow.ts +models/FlowDiagram.ts +models/FlowStageBinding.ts +models/Group.ts +models/GroupMembershipPolicy.ts +models/HaveIBeenPwendPolicy.ts +models/IPReputation.ts +models/IdentificationStage.ts +models/InlineResponse200.ts +models/InlineResponse2001.ts +models/InlineResponse20010.ts +models/InlineResponse20011.ts +models/InlineResponse20012.ts +models/InlineResponse20013.ts +models/InlineResponse20014.ts +models/InlineResponse20015.ts +models/InlineResponse20016.ts +models/InlineResponse20017.ts +models/InlineResponse20018.ts +models/InlineResponse20019.ts +models/InlineResponse2002.ts +models/InlineResponse20020.ts +models/InlineResponse20021.ts +models/InlineResponse20022.ts +models/InlineResponse20023.ts +models/InlineResponse20024.ts +models/InlineResponse20025.ts +models/InlineResponse20026.ts +models/InlineResponse20027.ts +models/InlineResponse20028.ts +models/InlineResponse20029.ts +models/InlineResponse2003.ts +models/InlineResponse20030.ts +models/InlineResponse20031.ts +models/InlineResponse20032.ts +models/InlineResponse20033.ts +models/InlineResponse20034.ts +models/InlineResponse20035.ts +models/InlineResponse20036.ts +models/InlineResponse20037.ts +models/InlineResponse20038.ts +models/InlineResponse20039.ts +models/InlineResponse2004.ts +models/InlineResponse20040.ts +models/InlineResponse20041.ts +models/InlineResponse20042.ts +models/InlineResponse20043.ts +models/InlineResponse20044.ts +models/InlineResponse20045.ts +models/InlineResponse20046.ts +models/InlineResponse20047.ts +models/InlineResponse20048.ts +models/InlineResponse20049.ts +models/InlineResponse2005.ts +models/InlineResponse20050.ts +models/InlineResponse20051.ts +models/InlineResponse20052.ts +models/InlineResponse20053.ts +models/InlineResponse20054.ts +models/InlineResponse20055.ts +models/InlineResponse20056.ts +models/InlineResponse20057.ts +models/InlineResponse20058.ts +models/InlineResponse20059.ts +models/InlineResponse2006.ts +models/InlineResponse20060.ts +models/InlineResponse2007.ts +models/InlineResponse2008.ts +models/InlineResponse2009.ts +models/InlineResponse200Pagination.ts +models/Invitation.ts +models/InvitationStage.ts +models/KubernetesServiceConnection.ts +models/LDAPPropertyMapping.ts +models/LDAPSource.ts +models/LDAPSourceSyncStatus.ts +models/LoginMetrics.ts +models/Notification.ts +models/NotificationRule.ts +models/NotificationRuleGroup.ts +models/NotificationRuleGroupParent.ts +models/NotificationRuleTransports.ts +models/NotificationTransport.ts +models/NotificationTransportTest.ts +models/OAuth2Provider.ts +models/OAuth2ProviderSetupURLs.ts +models/OAuthSource.ts +models/OpenIDConnectConfiguration.ts +models/Outpost.ts +models/OutpostHealth.ts +models/PasswordExpiryPolicy.ts +models/PasswordPolicy.ts +models/PasswordStage.ts +models/Policy.ts +models/PolicyBinding.ts +models/PolicyBindingPolicy.ts +models/PolicyBindingUser.ts +models/PolicyBindingUserAkGroups.ts +models/PolicyBindingUserGroups.ts +models/PolicyBindingUserSources.ts +models/PolicyBindingUserUserPermissions.ts +models/Prompt.ts +models/PromptStage.ts +models/PropertyMapping.ts +models/Provider.ts +models/ProxyOutpostConfig.ts +models/ProxyProvider.ts +models/ReputationPolicy.ts +models/SAMLMetadata.ts +models/SAMLPropertyMapping.ts +models/SAMLProvider.ts +models/SAMLSource.ts +models/ScopeMapping.ts +models/ServiceConnection.ts +models/ServiceConnectionState.ts +models/Source.ts +models/Stage.ts +models/Task.ts +models/Token.ts +models/TokenView.ts +models/TypeCreate.ts +models/User.ts +models/UserDeleteStage.ts +models/UserLoginStage.ts +models/UserLogoutStage.ts +models/UserReputation.ts +models/UserWriteStage.ts +models/Version.ts +models/index.ts +runtime.ts diff --git a/web/src/api/.openapi-generator/VERSION b/web/src/api/.openapi-generator/VERSION new file mode 100644 index 000000000..c30f0ec2b --- /dev/null +++ b/web/src/api/.openapi-generator/VERSION @@ -0,0 +1 @@ +5.1.0-SNAPSHOT \ No newline at end of file diff --git a/web/src/api/Applications.ts b/web/src/api/Applications.ts deleted file mode 100644 index cf10fdca0..000000000 --- a/web/src/api/Applications.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; -import { Provider } from "./Providers"; - -export class Application { - pk: string; - name: string; - slug: string; - provider?: Provider; - - launch_url: string; - meta_launch_url: string; - meta_icon: string; - meta_description: string; - meta_publisher: string; - policies: string[]; - - constructor() { - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["core", "applications", slug]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["core", "applications"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/applications/${rest}`; - } -} diff --git a/web/src/api/CertificateKeyPair.ts b/web/src/api/CertificateKeyPair.ts deleted file mode 100644 index 4681f8c0d..000000000 --- a/web/src/api/CertificateKeyPair.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; - -export class CertificateKeyPair { - pk: string; - name: string; - fingerprint: string; - cert_expiry: number; - cert_subject: string; - private_key_available: boolean; - - constructor() { - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["crypto", "certificatekeypairs", slug]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["crypto", "certificatekeypairs"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/crypto/certificates/${rest}`; - } -} diff --git a/web/src/api/Client.ts b/web/src/api/Client.ts index 6f6359d69..bdc6236c9 100644 --- a/web/src/api/Client.ts +++ b/web/src/api/Client.ts @@ -1,10 +1,3 @@ -import { gettext } from "django"; -import { showMessage } from "../elements/messages/MessageContainer"; -import { getCookie } from "../utils"; -import { NotFoundError, RequestError } from "./Error"; - -export const VERSION = "v2beta"; - export interface QueryArguments { page?: number; page_size?: number; @@ -13,104 +6,27 @@ export interface QueryArguments { export interface BaseInheritanceModel { - object_type: string; + objectType: string; - verbose_name: string; - verbose_name_plural: string; + verboseName: string; + verboseNamePlural: string; } -export class Client { - makeUrl(url: string[], query?: QueryArguments): string { - let builtUrl = `/api/${VERSION}/${url.join("/")}/`; - if (query) { - const queryString = Object.keys(query) - .filter((k) => query[k] !== null) - // we default to a string in query[k] as we've filtered out the null above - // this is just for type-hinting - .map((k) => encodeURIComponent(k) + "=" + encodeURIComponent(query[k] || "")) - .join("&"); - builtUrl += `?${queryString}`; - } - return builtUrl; - } - - fetch(url: string[], query?: QueryArguments): Promise { - const finalUrl = this.makeUrl(url, query); - return fetch(finalUrl) - .then((r) => { - if (r.status > 300) { - switch (r.status) { - case 404: - throw new NotFoundError(`URL ${finalUrl} not found`); - default: - throw new RequestError(r.statusText); - } - } - return r; - }) - .catch((e) => { - showMessage({ - level_tag: "error", - message: gettext(`Unexpected error while fetching: ${e.toString()}`), - }); - return e; - }) - .then((r) => r.json()) - .then((r) => r); - } - - private writeRequest(url: string[], body: T, method: string, query?: QueryArguments): Promise { - const finalUrl = this.makeUrl(url, query); - const csrftoken = getCookie("authentik_csrf"); - const request = new Request(finalUrl, { - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - "X-CSRFToken": csrftoken, - }, - }); - return fetch(request, { - method: method, - mode: "same-origin", - body: JSON.stringify(body), - }) - .then((r) => { - if (r.status > 300) { - switch (r.status) { - case 404: - throw new NotFoundError(`URL ${finalUrl} not found`); - default: - throw new RequestError(r.statusText); - } - } - return r; - }) - .then((r) => r.json()) - .then((r) => r); - } - - update(url: string[], body: T, query?: QueryArguments): Promise { - return this.writeRequest(url, body, "PATCH", query); - } -} - -export const DefaultClient = new Client(); - -export interface PBPagination { +export interface AKPagination { next?: number; previous?: number; count: number; current: number; - total_pages: number; + totalPages: number; - start_index: number; - end_index: number; + startIndex: number; + endIndex: number; } export interface AKResponse { - pagination: PBPagination; + pagination: AKPagination; results: Array; } diff --git a/web/src/api/Config.ts b/web/src/api/Config.ts index cac6dc067..2d4432d4a 100644 --- a/web/src/api/Config.ts +++ b/web/src/api/Config.ts @@ -1,42 +1,39 @@ -import { DefaultClient } from "./Client"; import * as Sentry from "@sentry/browser"; import { Integrations } from "@sentry/tracing"; import { VERSION } from "../constants"; import { SentryIgnoredError } from "../common/errors"; +import { Configuration } from "./runtime"; +import { RootApi } from "./apis"; +import { Config } from "."; +import { getCookie } from "../utils"; -export class Config { - branding_logo: string; - branding_title: string; - - error_reporting_enabled: boolean; - error_reporting_environment: string; - error_reporting_send_pii: boolean; - - constructor() { - throw Error(); +export const DEFAULT_CONFIG = new Configuration({ + basePath: "/api/v2beta", + headers: { + "X-CSRFToken": getCookie("authentik_csrf"), } +}); - static get(): Promise { - return DefaultClient.fetch(["root", "config"]).then((config) => { - if (config.error_reporting_enabled) { - Sentry.init({ - dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8", - release: `authentik@${VERSION}`, - integrations: [ - new Integrations.BrowserTracing(), - ], - tracesSampleRate: 0.6, - environment: config.error_reporting_environment, - beforeSend(event: Sentry.Event, hint: Sentry.EventHint) { - if (hint.originalException instanceof SentryIgnoredError) { - return null; - } - return event; - }, - }); - console.debug("authentik/config: Sentry enabled."); - } - return config; - }); - } +export function configureSentry(): Promise { + return new RootApi(DEFAULT_CONFIG).rootConfigList().then((config) => { + if (config.errorReportingEnabled) { + Sentry.init({ + dsn: "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8", + release: `authentik@${VERSION}`, + integrations: [ + new Integrations.BrowserTracing(), + ], + tracesSampleRate: 0.6, + environment: config.errorReportingEnvironment, + beforeSend(event: Sentry.Event, hint: Sentry.EventHint) { + if (hint.originalException instanceof SentryIgnoredError) { + return null; + } + return event; + }, + }); + console.debug("authentik/config: Sentry enabled."); + } + return config; + }); } diff --git a/web/src/api/EventNotification.ts b/web/src/api/EventNotification.ts deleted file mode 100644 index c1171e319..000000000 --- a/web/src/api/EventNotification.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DefaultClient, QueryArguments, AKResponse } from "./Client"; -import { Event } from "./Events"; - -export class Notification { - pk: string; - severity: string; - body: string; - created: string; - event?: Event; - seen: boolean; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["events", "notifications", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["events", "notifications"], filter); - } - - static markSeen(pk: string): Promise<{seen: boolean}> { - return DefaultClient.update(["events", "notifications", pk], { - "seen": true - }); - } - -} diff --git a/web/src/api/EventRules.ts b/web/src/api/EventRules.ts deleted file mode 100644 index e93e49994..000000000 --- a/web/src/api/EventRules.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DefaultClient, QueryArguments, AKResponse } from "./Client"; -import { Group } from "./Groups"; - -export class Rule { - pk: string; - name: string; - transports: string[]; - severity: string; - group?: Group; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["events", "rules", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["events", "rules"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/events/rules/${rest}`; - } -} diff --git a/web/src/api/EventTransports.ts b/web/src/api/EventTransports.ts deleted file mode 100644 index e40ac1b1b..000000000 --- a/web/src/api/EventTransports.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { DefaultClient, QueryArguments, AKResponse } from "./Client"; - -export class Transport { - pk: string; - name: string; - mode: string; - mode_verbose: string; - webhook_url: string; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["events", "transports", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["events", "transports"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/events/transports/${rest}`; - } -} diff --git a/web/src/api/Events.ts b/web/src/api/Events.ts index bc1cafb41..8b021fc20 100644 --- a/web/src/api/Events.ts +++ b/web/src/api/Events.ts @@ -1,4 +1,4 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; +import { Event } from "./models"; export interface EventUser { pk: number; @@ -11,37 +11,7 @@ export interface EventContext { [key: string]: EventContext | string | number | string[]; } -export class Event { - pk: string; +export interface EventWithContext extends Event { user: EventUser; - action: string; - app: string; context: EventContext; - client_ip: string; - created: string; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["events", "events", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["events", "events"], filter); - } - - // events/events/top_per_user/?filter_action=authorize_application - static topForUser(action: string): Promise { - return DefaultClient.fetch(["events", "events", "top_per_user"], { - "filter_action": action, - }); - } -} - -export interface TopNEvent { - application: { [key: string]: string}; - counted_events: number; - unique_users: number; } diff --git a/web/src/api/Flows.ts b/web/src/api/Flows.ts index 226fed3a3..91e76f2a1 100644 --- a/web/src/api/Flows.ts +++ b/web/src/api/Flows.ts @@ -1,12 +1,4 @@ -import { DefaultClient, AKResponse, QueryArguments, BaseInheritanceModel } from "./Client"; -import { TypeCreate } from "./Providers"; - -export enum ChallengeTypes { - native = "native", - response = "response", - shell = "shell", - redirect = "redirect", -} +import { ChallengeTypeEnum } from "./models"; export interface Error { code: string; @@ -18,11 +10,12 @@ export interface ErrorDict { } export interface Challenge { - type: ChallengeTypes; + type: ChallengeTypeEnum; component?: string; title?: string; response_errors?: ErrorDict; } + export interface WithUserInfoChallenge extends Challenge { pending_user: string; pending_user_avatar: string; @@ -31,6 +24,7 @@ export interface WithUserInfoChallenge extends Challenge { export interface ShellChallenge extends Challenge { body: string; } + export interface RedirectChallenge extends Challenge { to: string; } @@ -44,104 +38,3 @@ export enum FlowDesignation { Recovery = "recovery", StageConfiguration = "stage_configuration", } - -export class Flow { - pk: string; - policybindingmodel_ptr_id: string; - name: string; - slug: string; - title: string; - designation: FlowDesignation; - background: string; - stages: string[]; - policies: string[]; - cache_count: number; - - constructor() { - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["flows", "instances", slug]); - } - - static diagram(slug: string): Promise<{ diagram: string }> { - return DefaultClient.fetch<{ diagram: string }>(["flows", "instances", slug, "diagram"]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["flows", "instances"], filter); - } - - static cached(): Promise { - return DefaultClient.fetch<{ count: number }>(["flows", "instances", "cached"]).then(r => { - return r.count; - }); - } - - static executor(slug: string): Promise { - return DefaultClient.fetch(["flows", "executor", slug]); - } - - static adminUrl(rest: string): string { - return `/administration/flows/${rest}`; - } -} - -export class Stage implements BaseInheritanceModel { - pk: string; - name: string; - object_type: string; - verbose_name: string; - verbose_name_plural: string; - flow_set: Flow[]; - - constructor() { - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["stages", "all", slug]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["stages", "all"], filter); - } - - static getTypes(): Promise { - return DefaultClient.fetch(["stages", "all", "types"]); - } - - static adminUrl(rest: string): string { - return `/administration/stages/${rest}`; - } -} - -export class FlowStageBinding { - - pk: string; - policybindingmodel_ptr_id: string; - target: string; - stage: string; - stage_obj: Stage; - evaluate_on_plan: boolean; - re_evaluate_policies: boolean; - order: number; - policies: string[]; - - constructor() { - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["flows", "bindings", slug]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["flows", "bindings"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/stages/bindings/${rest}`; - } -} diff --git a/web/src/api/Groups.ts b/web/src/api/Groups.ts deleted file mode 100644 index 1d85c05b0..000000000 --- a/web/src/api/Groups.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DefaultClient, QueryArguments, AKResponse } from "./Client"; -import { EventContext } from "./Events"; - -export class Group { - - pk: string; - name: string; - is_superuser: boolean; - attributes: EventContext; - parent?: Group; - users: number[]; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["core", "groups", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["core", "groups"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/groups/${rest}`; - } -} diff --git a/web/src/api/Invitations.ts b/web/src/api/Invitations.ts deleted file mode 100644 index e28bd8c25..000000000 --- a/web/src/api/Invitations.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { DefaultClient, QueryArguments, AKResponse } from "./Client"; -import { EventContext } from "./Events"; -import { User } from "./Users"; - -export class Invitation { - - pk: string; - expires: number; - fixed_date: EventContext; - created_by: User; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["stages", "invitation", "invitations", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["stages", "invitation", "invitations"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/stages/invitations/${rest}`; - } -} diff --git a/web/src/api/Outposts.ts b/web/src/api/Outposts.ts deleted file mode 100644 index 10c60ed7e..000000000 --- a/web/src/api/Outposts.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; -import { Provider, TypeCreate } from "./Providers"; - -export interface OutpostHealth { - last_seen: number; - version: string; - version_should: string; - version_outdated: boolean; -} - -export class Outpost { - - pk: string; - name: string; - providers: number[]; - providers_obj: Provider[]; - service_connection?: string; - _config: QueryArguments; - token_identifier: string; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["outposts", "outposts", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["outposts", "outposts"], filter); - } - - static health(pk: string): Promise { - return DefaultClient.fetch(["outposts", "outposts", pk, "health"]); - } - - static adminUrl(rest: string): string { - return `/administration/outposts/${rest}`; - } -} - -export interface OutpostServiceConnectionState { - version: string; - healthy: boolean; -} - -export class OutpostServiceConnection { - pk: string; - name: string; - local: boolean; - object_type: string; - verbose_name: string; - verbose_name_plural: string; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["outposts", "service_connections", "all", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["outposts", "service_connections", "all"], filter); - } - - static state(pk: string): Promise { - return DefaultClient.fetch(["outposts", "service_connections", "all", pk, "state"]); - } - - static getTypes(): Promise { - return DefaultClient.fetch(["outposts", "service_connections", "all", "types"]); - } - - static adminUrl(rest: string): string { - return `/administration/outpost_service_connections/${rest}`; - } - -} diff --git a/web/src/api/Policies.ts b/web/src/api/Policies.ts deleted file mode 100644 index d720bd559..000000000 --- a/web/src/api/Policies.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { DefaultClient, BaseInheritanceModel, AKResponse, QueryArguments } from "./Client"; -import { TypeCreate } from "./Providers"; - -export class Policy implements BaseInheritanceModel { - pk: string; - name: string; - execution_logging: boolean; - object_type: string; - verbose_name: string; - verbose_name_plural: string; - bound_to: number; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["policies", "all", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["policies", "all"], filter); - } - - static cached(): Promise { - return DefaultClient.fetch<{ count: number }>(["policies", "all", "cached"]).then(r => { - return r.count; - }); - } - - static getTypes(): Promise { - return DefaultClient.fetch(["policies", "all", "types"]); - } - - static adminUrl(rest: string): string { - return `/administration/policies/${rest}`; - } -} diff --git a/web/src/api/PolicyBindings.ts b/web/src/api/PolicyBindings.ts deleted file mode 100644 index ca54d85e8..000000000 --- a/web/src/api/PolicyBindings.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; -import { Group } from "./Groups"; -import { Policy } from "./Policies"; -import { User } from "./Users"; - -export class PolicyBinding { - pk: string; - policy?: Policy; - group?: Group; - user?: User; - target: string; - enabled: boolean; - order: number; - timeout: number; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["policies", "bindings", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["policies", "bindings"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/policies/bindings/${rest}`; - } -} diff --git a/web/src/api/Prompts.ts b/web/src/api/Prompts.ts deleted file mode 100644 index ee2100717..000000000 --- a/web/src/api/Prompts.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DefaultClient, QueryArguments, AKResponse } from "./Client"; -import { Stage } from "./Flows"; - -export class Prompt { - - pk: string; - field_key: string; - label: string; - type: string; - required: boolean; - placeholder: string; - order: number; - promptstage_set: Stage[]; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["stages", "prompt", "prompts", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["stages", "prompt", "prompts"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/stages_prompts/${rest}`; - } -} diff --git a/web/src/api/PropertyMapping.ts b/web/src/api/PropertyMapping.ts deleted file mode 100644 index eae1f176f..000000000 --- a/web/src/api/PropertyMapping.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; -import { TypeCreate } from "./Providers"; - -export class PropertyMapping { - pk: string; - name: string; - expression: string; - - verbose_name: string; - verbose_name_plural: string; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["propertymappings", "all", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["propertymappings", "all"], filter); - } - - static getTypes(): Promise { - return DefaultClient.fetch(["propertymappings", "all", "types"]); - } - - static adminUrl(rest: string): string { - return `/administration/property-mappings/${rest}`; - } -} diff --git a/web/src/api/Providers.ts b/web/src/api/Providers.ts deleted file mode 100644 index 6a5e66946..000000000 --- a/web/src/api/Providers.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { BaseInheritanceModel, DefaultClient, AKResponse, QueryArguments } from "./Client"; - -export interface TypeCreate { - name: string; - description: string; - link: string; -} - -export class Provider implements BaseInheritanceModel { - pk: number; - name: string; - authorization_flow: string; - object_type: string; - - assigned_application_slug?: string; - assigned_application_name?: string; - - verbose_name: string; - verbose_name_plural: string; - - constructor() { - throw Error(); - } - - static get(id: number): Promise { - return DefaultClient.fetch(["providers", "all", id.toString()]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["providers", "all"], filter); - } - - static getTypes(): Promise { - return DefaultClient.fetch(["providers", "all", "types"]); - } - - static adminUrl(rest: string): string { - return `/administration/providers/${rest}`; - } -} diff --git a/web/src/api/Sources.ts b/web/src/api/Sources.ts deleted file mode 100644 index 0e05d2b5b..000000000 --- a/web/src/api/Sources.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { BaseInheritanceModel, DefaultClient, AKResponse, QueryArguments } from "./Client"; -import { TypeCreate } from "./Providers"; - -export class Source implements BaseInheritanceModel { - pk: string; - name: string; - slug: string; - enabled: boolean; - authentication_flow: string; - enrollment_flow: string; - - constructor() { - throw Error(); - } - object_type: string; - verbose_name: string; - verbose_name_plural: string; - - static get(slug: string): Promise { - return DefaultClient.fetch(["sources", "all", slug]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["sources", "all"], filter); - } - - static getTypes(): Promise { - return DefaultClient.fetch(["sources", "all", "types"]); - } - - static adminUrl(rest: string): string { - return `/administration/sources/${rest}`; - } -} diff --git a/web/src/api/SystemTask.ts b/web/src/api/SystemTask.ts deleted file mode 100644 index 222873286..000000000 --- a/web/src/api/SystemTask.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DefaultClient, QueryArguments } from "./Client"; - -export enum TaskStatus { - SUCCESSFUL = 1, - WARNING = 2, - ERROR = 4, -} - -export class SystemTask { - - task_name: string; - task_description: string; - task_finish_timestamp: number; - status: TaskStatus; - messages: string[]; - - constructor() { - throw Error(); - } - - static get(task_name: string): Promise { - return DefaultClient.fetch(["admin", "system_tasks", task_name]); - } - - static list(filter?: QueryArguments): Promise { - return DefaultClient.fetch(["admin", "system_tasks"], filter); - } - - static retry(task_name: string): string { - return DefaultClient.makeUrl(["admin", "system_tasks", task_name, "retry"]); - } - -} diff --git a/web/src/api/Tokens.ts b/web/src/api/Tokens.ts deleted file mode 100644 index 9319efa7d..000000000 --- a/web/src/api/Tokens.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { AKResponse, DefaultClient, QueryArguments } from "./Client"; -import { User } from "./Users"; - -export enum TokenIntent { - INTENT_VERIFICATION = "verification", - INTENT_API = "api", - INTENT_RECOVERY = "recovery", -} - -export class Token { - - pk: string; - identifier: string; - intent: TokenIntent; - user: User; - description: string; - - expires: number; - expiring: boolean; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["core", "tokens", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["core", "tokens"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/tokens/${rest}`; - } - - static userUrl(rest: string): string { - return `/-/user/tokens/${rest}`; - } - - static getKey(identifier: string): Promise { - return DefaultClient.fetch<{ key: string }>(["core", "tokens", identifier, "view_key"]).then( - (r) => r.key - ); - } - -} diff --git a/web/src/api/Users.ts b/web/src/api/Users.ts index 6bf1a7604..f9b0de78b 100644 --- a/web/src/api/Users.ts +++ b/web/src/api/Users.ts @@ -1,45 +1,11 @@ -import { DefaultClient, AKResponse, QueryArguments } from "./Client"; +import { CoreApi } from "./apis"; +import { DEFAULT_CONFIG } from "./Config"; +import { User } from "./models"; let _globalMePromise: Promise; - -export class User { - pk: number; - username: string; - name: string; - is_superuser: boolean; - email: boolean; - avatar: string; - is_active: boolean; - last_login: number; - - constructor() { - throw Error(); - } - - static get(pk: string): Promise { - return DefaultClient.fetch(["core", "users", pk]); - } - - static list(filter?: QueryArguments): Promise> { - return DefaultClient.fetch>(["core", "users"], filter); - } - - static adminUrl(rest: string): string { - return `/administration/users/${rest}`; - } - - static me(): Promise { - if (!_globalMePromise) { - _globalMePromise = DefaultClient.fetch(["core", "users", "me"]); - } - return _globalMePromise; - } - - static count(): Promise { - return DefaultClient.fetch>(["core", "users"], { - "page_size": 1 - }).then(r => { - return r.pagination.count; - }); +export function me(): Promise { + if (!_globalMePromise) { + _globalMePromise = new CoreApi(DEFAULT_CONFIG).coreUsersMe({}); } + return _globalMePromise; } diff --git a/web/src/api/Versions.ts b/web/src/api/Versions.ts deleted file mode 100644 index df75dc1d2..000000000 --- a/web/src/api/Versions.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { DefaultClient } from "./Client"; - -export class Version { - - version_current: string; - version_latest: string; - outdated: boolean; - - constructor() { - throw Error(); - } - - static get(): Promise { - return DefaultClient.fetch(["admin", "version"]); - } - -} diff --git a/web/src/api/legacy.ts b/web/src/api/legacy.ts new file mode 100644 index 000000000..1d4e6ba0b --- /dev/null +++ b/web/src/api/legacy.ts @@ -0,0 +1,97 @@ +export class AdminURLManager { + + static applications(rest: string): string { + return `/administration/applications/${rest}`; + } + + static cryptoCertificates(rest: string): string { + return `/administration/crypto/certificates/${rest}`; + } + + static policies(rest: string): string { + return `/administration/policies/${rest}`; + } + + static policyBindings(rest: string): string { + return `/administration/policies/bindings/${rest}`; + } + + static providers(rest: string): string { + return `/administration/providers/${rest}`; + } + + static propertyMappings(rest: string): string { + return `/administration/property-mappings/${rest}`; + } + + static outposts(rest: string): string { + return `/administration/outposts/${rest}`; + } + + static outpostServiceConnections(rest: string): string { + return `/administration/outpost_service_connections/${rest}`; + } + + static flows(rest: string): string { + return `/administration/flows/${rest}`; + } + + static stages(rest: string): string { + return `/administration/stages/${rest}`; + } + + static stagePrompts(rest: string): string { + return `/administration/stages_prompts/${rest}`; + } + + static stageInvitations(rest: string): string { + return `/administration/stages/invitations/${rest}`; + } + + static stageBindings(rest: string): string { + return `/administration/stages/bindings/${rest}`; + } + + static sources(rest: string): string { + return `/administration/sources/${rest}`; + } + + static tokens(rest: string): string { + return `/administration/tokens/${rest}`; + } + + static eventRules(rest: string): string { + return `/administration/events/rules/${rest}`; + } + + static eventTransports(rest: string): string { + return `/administration/events/transports/${rest}`; + } + + static users(rest: string): string { + return `/administration/users/${rest}`; + } + + static groups(rest: string): string { + return `/administration/groups/${rest}`; + } +} + +export class UserURLManager { + + static tokens(rest: string): string { + return `/-/user/tokens/${rest}`; + } + +} + +export class AppURLManager { + + static sourceSAML(slug: string, rest: string): string { + return `/source/saml/${slug}/${rest}`; + } + static providerSAML(rest: string): string { + return `/application/saml/${rest}`; + } + +} diff --git a/web/src/api/providers/OAuth2.ts b/web/src/api/providers/OAuth2.ts deleted file mode 100644 index 3216d03cc..000000000 --- a/web/src/api/providers/OAuth2.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { DefaultClient } from "../Client"; -import { Provider } from "../Providers"; - -export interface OAuth2SetupURLs { - - issuer?: string; - authorize: string; - token: string; - user_info: string; - provider_info?: string; - logout?: string; - -} - -export class OAuth2Provider extends Provider { - client_type: string - client_id: string; - client_secret: string; - token_validity: string; - include_claims_in_id_token: boolean; - jwt_alg: string; - rsa_key: string; - redirect_uris: string; - sub_mode: string; - issuer_mode: string; - - constructor() { - super(); - throw Error(); - } - - static get(id: number): Promise { - return DefaultClient.fetch(["providers", "oauth2", id.toString()]); - } - - static getLaunchURls(id: number): Promise { - return DefaultClient.fetch(["providers", "oauth2", id.toString(), "setup_urls"]); - } - - static appUrl(rest: string): string { - return `/application/oauth2/${rest}`; - } -} diff --git a/web/src/api/providers/Proxy.ts b/web/src/api/providers/Proxy.ts deleted file mode 100644 index dad108593..000000000 --- a/web/src/api/providers/Proxy.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DefaultClient } from "../Client"; -import { Provider } from "../Providers"; - -export class ProxyProvider extends Provider { - internal_host: string; - external_host: string; - internal_host_ssl_validation: boolean - certificate?: string; - skip_path_regex: string; - basic_auth_enabled: boolean; - basic_auth_password_attribute: string; - basic_auth_user_attribute: string; - - constructor() { - super(); - throw Error(); - } - - static get(id: number): Promise { - return DefaultClient.fetch(["providers", "proxy", id.toString()]); - } - - static getMetadata(id: number): Promise<{ metadata: string }> { - return DefaultClient.fetch(["providers", "proxy", id.toString(), "metadata"]); - } - - static appUrl(rest: string): string { - return `/application/proxy/${rest}`; - } -} diff --git a/web/src/api/providers/SAML.ts b/web/src/api/providers/SAML.ts deleted file mode 100644 index 814653b81..000000000 --- a/web/src/api/providers/SAML.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DefaultClient } from "../Client"; -import { Provider } from "../Providers"; - -export class SAMLProvider extends Provider { - acs_url: string; - audience: string; - issuer: string; - assertion_valid_not_before: string; - assertion_valid_not_on_or_after: string; - session_valid_not_on_or_after: string; - name_id_mapping?: string; - digest_algorithm: string; - signature_algorithm: string; - signing_kp?: string; - verification_kp?: string; - - constructor() { - super(); - throw Error(); - } - - static get(id: number): Promise { - return DefaultClient.fetch(["providers", "saml", id.toString()]); - } - - static getMetadata(id: number): Promise<{ metadata: string }> { - return DefaultClient.fetch(["providers", "saml", id.toString(), "metadata"]); - } - - static appUrl(rest: string): string { - return `/application/saml/${rest}`; - } -} diff --git a/web/src/api/sources/LDAP.ts b/web/src/api/sources/LDAP.ts deleted file mode 100644 index 7d4c427c3..000000000 --- a/web/src/api/sources/LDAP.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { DefaultClient } from "../Client"; -import { Source } from "../Sources"; - -export class LDAPSource extends Source { - server_uri: string; - bind_cn: string; - start_tls: boolean - base_dn: string; - additional_user_dn: string; - additional_group_dn: string; - user_object_filter: string; - group_object_filter: string; - group_membership_field: string; - object_uniqueness_field: string; - sync_users: boolean; - sync_users_password: boolean; - sync_groups: boolean; - sync_parent_group?: string; - property_mappings: string[]; - property_mappings_group: string[]; - - constructor() { - super(); - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["sources", "ldap", slug]); - } - - static syncStatus(slug: string): Promise<{ last_sync?: number }> { - return DefaultClient.fetch(["sources", "ldap", slug, "sync_status"]); - } - -} diff --git a/web/src/api/sources/OAuth.ts b/web/src/api/sources/OAuth.ts deleted file mode 100644 index 216d9c202..000000000 --- a/web/src/api/sources/OAuth.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { DefaultClient } from "../Client"; -import { Source } from "../Sources"; - -export class OAuthSource extends Source { - provider_type: string; - request_token_url: string; - authorization_url: string; - access_token_url: string; - profile_url: string; - consumer_key: string; - callback_url: string; - - constructor() { - super(); - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["sources", "oauth", slug]); - } - -} diff --git a/web/src/api/sources/SAML.ts b/web/src/api/sources/SAML.ts deleted file mode 100644 index 949f37b3c..000000000 --- a/web/src/api/sources/SAML.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { DefaultClient } from "../Client"; -import { Source } from "../Sources"; - -export class SAMLSource extends Source { - issuer: string; - sso_url: string; - slo_url: string; - allow_idp_initiated: boolean; - name_id_policy: string; - binding_type: string - signing_kp?: string; - digest_algorithm: string; - signature_algorithm: string; - temporary_user_delete_after: string; - - constructor() { - super(); - throw Error(); - } - - static get(slug: string): Promise { - return DefaultClient.fetch(["sources", "saml", slug]); - } - - static getMetadata(slug: string): Promise<{ metadata: string }> { - return DefaultClient.fetch(["sources", "saml", slug, "metadata"]); - } - - static appUrl(slug: string, rest: string): string { - return `/source/saml/${slug}/${rest}`; - } -} diff --git a/web/src/authentik.css b/web/src/authentik.css index f5edceb1c..c391694a9 100644 --- a/web/src/authentik.css +++ b/web/src/authentik.css @@ -5,6 +5,12 @@ html { --pf-c-nav__link--PaddingLeft: 0.5rem; } +html > input { + position: absolute; + top: -2000px; + left: -2000px; +} + .pf-c-page__header { z-index: 0; } @@ -83,7 +89,7 @@ select[multiple] { /* ensure background on non-flow pages match */ .pf-c-background-image::before { - background-image: url("dist/assets/images/flow_background.jpg"); + background-image: url("/static/dist/assets/images/flow_background.jpg"); background-position: center; } @@ -157,7 +163,7 @@ ak-message { color: var(--ak-dark-foreground) !important; } /* tabs, vertical */ - .pf-c-tabs__link { + .pf-c-tabs.pf-m-vertical .pf-c-tabs__link { background-color: var(--ak-dark-background-light); } /* table, on mobile */ diff --git a/web/src/elements/AdminLoginsChart.ts b/web/src/elements/AdminLoginsChart.ts deleted file mode 100644 index 2d82dbf27..000000000 --- a/web/src/elements/AdminLoginsChart.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; -import Chart from "chart.js"; -import { DefaultClient } from "../api/Client"; - -interface TickValue { - value: number; - major: boolean; -} - -export interface LoginMetrics { - logins_failed_per_1h: { x: number, y: number }[]; - logins_per_1h: { x: number, y: number }[]; -} - -@customElement("ak-admin-logins-chart") -export class AdminLoginsChart extends LitElement { - @property({type: Array}) - url: string[] = []; - - chart?: Chart; - - static get styles(): CSSResult[] { - return [css` - :host { - position: relative; - height: 100%; - width: 100%; - display: block; - min-height: 25rem; - } - canvas { - width: 100px; - height: 100px; - } - `]; - } - - constructor() { - super(); - window.addEventListener("resize", () => { - if (this.chart) { - this.chart.resize(); - } - }); - } - - firstUpdated(): void { - DefaultClient.fetch(this.url) - .then((r) => { - const canvas = this.shadowRoot?.querySelector("canvas"); - if (!canvas) { - console.warn("Failed to get canvas element"); - return false; - } - const ctx = canvas.getContext("2d"); - if (!ctx) { - console.warn("failed to get 2d context"); - return false; - } - this.chart = new Chart(ctx, { - type: "bar", - data: { - datasets: [ - { - label: "Failed Logins", - backgroundColor: "rgba(201, 25, 11, .5)", - spanGaps: true, - data: r.logins_failed_per_1h, - }, - { - label: "Successful Logins", - backgroundColor: "rgba(189, 229, 184, .5)", - spanGaps: true, - data: r.logins_per_1h, - }, - ], - }, - options: { - maintainAspectRatio: false, - spanGaps: true, - scales: { - xAxes: [ - { - stacked: true, - gridLines: { - color: "rgba(0, 0, 0, 0)", - }, - type: "time", - offset: true, - ticks: { - callback: function (value, index: number, values) { - const valueStamp = (values[index]); - const delta = Date.now() - valueStamp.value; - const ago = Math.round(delta / 1000 / 3600); - return `${ago} Hours ago`; - }, - autoSkip: true, - maxTicksLimit: 8, - }, - }, - ], - yAxes: [ - { - stacked: true, - gridLines: { - color: "rgba(0, 0, 0, 0)", - }, - }, - ], - }, - }, - }); - }); - } - - render(): TemplateResult { - return html``; - } -} diff --git a/web/src/elements/buttons/ActionButton.ts b/web/src/elements/buttons/ActionButton.ts index 280409ce2..02f13eb50 100644 --- a/web/src/elements/buttons/ActionButton.ts +++ b/web/src/elements/buttons/ActionButton.ts @@ -1,4 +1,3 @@ -import { getCookie } from "../../utils"; import { customElement, property } from "lit-element"; import { ERROR_CLASS, SUCCESS_CLASS } from "../../constants"; import { SpinnerButton } from "./SpinnerButton"; @@ -12,43 +11,33 @@ export class ActionButton extends SpinnerButton { @property() method = "POST"; + @property({attribute: false}) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + apiRequest: () => Promise = () => { throw new Error(); }; + callAction(): void { if (this.isRunning === true) { return; } this.setLoading(); - const csrftoken = getCookie("authentik_csrf"); - const request = new Request(this.url, { - headers: { "X-CSRFToken": csrftoken }, - }); - fetch(request, { - method: this.method, - mode: "same-origin", + this.apiRequest().then(() => { + this.setDone(SUCCESS_CLASS); }) - .then((r) => { - if (!r.ok) { - throw r; - } - return r; - }) - .then(() => { - this.setDone(SUCCESS_CLASS); - }) - .catch((e: Error | Response) => { - if (e instanceof Error) { + .catch((e: Error | Response) => { + if (e instanceof Error) { + showMessage({ + level_tag: "error", + message: e.toString() + }); + } else { + e.text().then(t => { showMessage({ level_tag: "error", - message: e.toString() + message: t }); - } else { - e.text().then(t => { - showMessage({ - level_tag: "error", - message: t - }); - }); - } - this.setDone(ERROR_CLASS); - }); + }); + } + this.setDone(ERROR_CLASS); + }); } } diff --git a/web/src/elements/buttons/TokenCopyButton.ts b/web/src/elements/buttons/TokenCopyButton.ts index ee8b214df..6d203e5dc 100644 --- a/web/src/elements/buttons/TokenCopyButton.ts +++ b/web/src/elements/buttons/TokenCopyButton.ts @@ -3,9 +3,10 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; // @ts-ignore import ButtonStyle from "@patternfly/patternfly/components/Button/button.css"; -import { Token } from "../../api/Tokens"; +import { CoreApi } from "../../api"; import { ERROR_CLASS, PRIMARY_CLASS, SUCCESS_CLASS } from "../../constants"; import { ColorStyles } from "../../common/styles"; +import { DEFAULT_CONFIG } from "../../api/Config"; @customElement("ak-token-copy-button") export class TokenCopyButton extends LitElement { @@ -36,8 +37,14 @@ export class TokenCopyButton extends LitElement { }, 1500); return; } - Token.getKey(this.identifier).then((token) => { - navigator.clipboard.writeText(token).then(() => { + new CoreApi(DEFAULT_CONFIG).coreTokensViewKey({ + identifier: this.identifier + }).then((token) => { + if (!token.key) { + this.buttonClass = ERROR_CLASS; + return; + } + navigator.clipboard.writeText(token.key).then(() => { this.buttonClass = SUCCESS_CLASS; setTimeout(() => { this.buttonClass = PRIMARY_CLASS; diff --git a/web/src/elements/charts/AdminLoginsChart.ts b/web/src/elements/charts/AdminLoginsChart.ts new file mode 100644 index 000000000..5bad3923f --- /dev/null +++ b/web/src/elements/charts/AdminLoginsChart.ts @@ -0,0 +1,41 @@ +import { customElement } from "lit-element"; +import Chart from "chart.js"; +import { AdminApi, LoginMetrics } from "../../api"; +import { AKChart } from "./Chart"; +import { DEFAULT_CONFIG } from "../../api/Config"; + +@customElement("ak-charts-admin-login") +export class AdminLoginsChart extends AKChart { + + apiRequest(): Promise { + return new AdminApi(DEFAULT_CONFIG).adminMetricsList(); + } + + getDatasets(data: LoginMetrics): Chart.ChartDataSets[] { + return [ + { + label: "Failed Logins", + backgroundColor: "rgba(201, 25, 11, .5)", + spanGaps: true, + data: data.loginsFailedPer1h?.map((cord) => { + return { + x: cord.xCord, + y: cord.yCord, + }; + }), + }, + { + label: "Successful Logins", + backgroundColor: "rgba(189, 229, 184, .5)", + spanGaps: true, + data: data.loginsPer1h?.map((cord) => { + return { + x: cord.xCord, + y: cord.yCord, + }; + }), + }, + ]; + } + +} diff --git a/web/src/elements/charts/ApplicationAuthorizeChart.ts b/web/src/elements/charts/ApplicationAuthorizeChart.ts new file mode 100644 index 000000000..a72bf44d9 --- /dev/null +++ b/web/src/elements/charts/ApplicationAuthorizeChart.ts @@ -0,0 +1,32 @@ +import { customElement, property } from "lit-element"; +import { Coordinate, CoreApi } from "../../api"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { AKChart } from "./Chart"; + +@customElement("ak-charts-application-authorize") +export class ApplicationAuthorizeChart extends AKChart { + + @property() + applicationSlug!: string; + + apiRequest(): Promise { + return new CoreApi(DEFAULT_CONFIG).coreApplicationsMetrics({ slug: this.applicationSlug }); + } + + getDatasets(data: Coordinate[]): Chart.ChartDataSets[] { + return [ + { + label: "Authorizations", + backgroundColor: "rgba(189, 229, 184, .5)", + spanGaps: true, + data: data.map((cord) => { + return { + x: cord.xCord, + y: cord.yCord, + }; + }), + }, + ]; + } + +} diff --git a/web/src/elements/charts/Chart.ts b/web/src/elements/charts/Chart.ts new file mode 100644 index 000000000..a4fec4550 --- /dev/null +++ b/web/src/elements/charts/Chart.ts @@ -0,0 +1,103 @@ +import { css, CSSResult, html, LitElement, TemplateResult } from "lit-element"; +import Chart from "chart.js"; + +interface TickValue { + value: number; + major: boolean; +} + +export abstract class AKChart extends LitElement { + + abstract apiRequest(): Promise; + abstract getDatasets(data: T): Chart.ChartDataSets[]; + + chart?: Chart; + + static get styles(): CSSResult[] { + return [css` + :host { + position: relative; + height: 100%; + width: 100%; + display: block; + min-height: 25rem; + } + canvas { + width: 100px; + height: 100px; + } + `]; + } + + constructor() { + super(); + window.addEventListener("resize", () => { + if (this.chart) { + this.chart.resize(); + } + }); + } + + configureChart(data: T, ctx: CanvasRenderingContext2D): Chart { + return new Chart(ctx, { + type: "bar", + data: { + datasets: this.getDatasets(data), + }, + options: { + maintainAspectRatio: false, + spanGaps: true, + scales: { + xAxes: [ + { + stacked: true, + gridLines: { + color: "rgba(0, 0, 0, 0)", + }, + type: "time", + offset: true, + ticks: { + callback: function (value, index: number, values) { + const valueStamp = (values[index]); + const delta = Date.now() - valueStamp.value; + const ago = Math.round(delta / 1000 / 3600); + return `${ago} Hours ago`; + }, + autoSkip: true, + maxTicksLimit: 8, + }, + }, + ], + yAxes: [ + { + stacked: true, + gridLines: { + color: "rgba(0, 0, 0, 0)", + }, + }, + ], + }, + }, + }); + } + + firstUpdated(): void { + this.apiRequest().then((r) => { + const canvas = this.shadowRoot?.querySelector("canvas"); + if (!canvas) { + console.warn("Failed to get canvas element"); + return false; + } + const ctx = canvas.getContext("2d"); + if (!ctx) { + console.warn("failed to get 2d context"); + return false; + } + this.chart = this.configureChart(r, ctx); + }); + } + + render(): TemplateResult { + return html``; + } +} diff --git a/web/src/elements/messages/Message.ts b/web/src/elements/messages/Message.ts index 50d3e3b78..55adf7ea9 100644 --- a/web/src/elements/messages/Message.ts +++ b/web/src/elements/messages/Message.ts @@ -46,6 +46,15 @@ export class Message extends LitElement {

${this.message?.message}

+
+ +
`; } diff --git a/web/src/elements/messages/MessageContainer.ts b/web/src/elements/messages/MessageContainer.ts index 2f66d8721..3ed15286a 100644 --- a/web/src/elements/messages/MessageContainer.ts +++ b/web/src/elements/messages/MessageContainer.ts @@ -8,7 +8,7 @@ export function showMessage(message: APIMessage): void { if (!container) { throw new Error("failed to find message container"); } - container.messages.push(message); + container.addMessage(message); container.requestUpdate(); } @@ -34,7 +34,16 @@ export class MessageContainer extends LitElement { } } + // add a new message, but only if the message isn't currently shown. + addMessage(message: APIMessage): void { + const matchingMessages = this.messages.filter(m => m.message == message.message); + if (matchingMessages.length < 1) { + this.messages.push(message); + } + } + connect(): void { + if (navigator.webdriver) return; const wsUrl = `${window.location.protocol.replace("http", "ws")}//${ window.location.host }/ws/client/`; @@ -59,7 +68,7 @@ export class MessageContainer extends LitElement { }); this.messageSocket.addEventListener("message", (e) => { const data = JSON.parse(e.data); - this.messages.push(data); + this.addMessage(data); this.requestUpdate(); }); this.messageSocket.addEventListener("error", (e) => { @@ -75,8 +84,8 @@ export class MessageContainer extends LitElement { .message=${m} .onRemove=${(m: APIMessage) => { this.messages = this.messages.filter((v) => v !== m); - this.requestUpdate(); - }}> + this.requestUpdate(); + }}> `; })} `; diff --git a/web/src/elements/notifications/NotificationDrawer.ts b/web/src/elements/notifications/NotificationDrawer.ts index 8b8e77ae8..6fe61c850 100644 --- a/web/src/elements/notifications/NotificationDrawer.ts +++ b/web/src/elements/notifications/NotificationDrawer.ts @@ -1,7 +1,8 @@ import { gettext } from "django"; import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; +import { EventsApi, Notification } from "../../api"; import { AKResponse } from "../../api/Client"; -import { Notification } from "../../api/EventNotification"; +import { DEFAULT_CONFIG } from "../../api/Config"; import { COMMON_STYLES } from "../../common/styles"; @customElement("ak-notification-drawer") @@ -30,9 +31,9 @@ export class NotificationDrawer extends LitElement { } firstUpdated(): void { - Notification.list({ - seen: false, - ordering: "-created" + new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({ + seen: "false", + ordering: "-created", }).then(r => { this.notifications = r; this.unread = r.results.length; @@ -40,7 +41,6 @@ export class NotificationDrawer extends LitElement { } renderItem(item: Notification): TemplateResult { - const created = new Date(parseInt(item.created, 10) * 1000); let level = ""; switch (item.severity) { case "notice": @@ -66,7 +66,12 @@ export class NotificationDrawer extends LitElement {

${item.body}

- ${created.toLocaleString()} + ${item.created?.toLocaleString()} `; } diff --git a/web/src/elements/policies/BoundPoliciesList.ts b/web/src/elements/policies/BoundPoliciesList.ts index 03d198cbe..0f9f344de 100644 --- a/web/src/elements/policies/BoundPoliciesList.ts +++ b/web/src/elements/policies/BoundPoliciesList.ts @@ -2,16 +2,16 @@ import { gettext } from "django"; import { customElement, html, property, TemplateResult } from "lit-element"; import { AKResponse } from "../../api/Client"; import { Table, TableColumn } from "../../elements/table/Table"; -import { PolicyBinding } from "../../api/PolicyBindings"; +import { PoliciesApi, PolicyBinding } from "../../api"; import "../../elements/Tabs"; -import "../../elements/AdminLoginsChart"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/Dropdown"; -import { Policy } from "../../api/Policies"; import { until } from "lit-html/directives/until"; import { PAGE_SIZE } from "../../constants"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { AdminURLManager } from "../../api/legacy"; @customElement("ak-bound-policies-list") export class BoundPoliciesList extends Table { @@ -19,11 +19,11 @@ export class BoundPoliciesList extends Table { target?: string; apiEndpoint(page: number): Promise> { - return PolicyBinding.list({ + return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({ target: this.target || "", ordering: "order", page: page, - page_size: PAGE_SIZE, + pageSize: PAGE_SIZE, }); } @@ -49,6 +49,33 @@ export class BoundPoliciesList extends Table { } } + getObjectEditButton(item: PolicyBinding): TemplateResult { + if (item.policy) { + return html` + + ${gettext("Edit Policy")} + +
+
`; + } else if (item.group) { + return html` + + ${gettext("Edit Group")} + +
+
`; + } else if (item.user) { + return html` + + ${gettext("Edit User")} + +
+
`; + } else { + return html``; + } + } + row(item: PolicyBinding): TemplateResult[] { return [ html`${this.getPolicyUserGroupRow(item)}`, @@ -56,15 +83,16 @@ export class BoundPoliciesList extends Table { html`${item.order}`, html`${item.timeout}`, html` - + ${this.getObjectEditButton(item)} + - ${gettext("Edit")} + ${gettext("Edit Binding")}
- + - ${gettext("Delete")} + ${gettext("Delete Binding")}
@@ -78,7 +106,7 @@ export class BoundPoliciesList extends Table { ${gettext("No policies are currently bound to this object.")}
- + ${gettext("Bind Policy")} @@ -96,7 +124,7 @@ export class BoundPoliciesList extends Table { - + ${gettext("Bind Policy")} diff --git a/web/src/elements/router/RouterOutlet.ts b/web/src/elements/router/RouterOutlet.ts index 796511c34..6aae6046e 100644 --- a/web/src/elements/router/RouterOutlet.ts +++ b/web/src/elements/router/RouterOutlet.ts @@ -50,18 +50,17 @@ export class RouterOutlet extends LitElement { if (activeUrl === "") { activeUrl = this.defaultUrl || "/"; window.location.hash = `#${activeUrl}`; - console.debug(`authentik/router: set to ${window.location.hash}`); + console.debug(`authentik/router: defaulted URL to ${window.location.hash}`); return; } let matchedRoute: RouteMatch | null = null; ROUTES.some((route) => { - console.debug(`authentik/router: matching ${activeUrl} against ${route.url}`); const match = route.url.exec(activeUrl); if (match != null) { matchedRoute = new RouteMatch(route); matchedRoute.arguments = match.groups || {}; matchedRoute.fullUrl = activeUrl; - console.debug(`authentik/router: found match ${matchedRoute}`); + console.debug("authentik/router: found match ", matchedRoute); return true; } }); diff --git a/web/src/elements/sidebar/SidebarBrand.ts b/web/src/elements/sidebar/SidebarBrand.ts index e55e17edd..1d9e5eef7 100644 --- a/web/src/elements/sidebar/SidebarBrand.ts +++ b/web/src/elements/sidebar/SidebarBrand.ts @@ -3,15 +3,17 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu import PageStyle from "@patternfly/patternfly/components/Page/page.css"; // @ts-ignore import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; -import { Config } from "../../api/Config"; +import { configureSentry } from "../../api/Config"; +import { Config } from "../../api"; +import { ifDefined } from "lit-html/directives/if-defined"; export const DefaultConfig: Config = { - branding_logo: " /static/dist/assets/icons/icon_left_brand.svg", - branding_title: "authentik", + brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg", + brandingTitle: "authentik", - error_reporting_enabled: false, - error_reporting_environment: "", - error_reporting_send_pii: false, + errorReportingEnabled: false, + errorReportingEnvironment: "", + errorReportingSendPii: false, }; @customElement("ak-sidebar-brand") @@ -40,13 +42,13 @@ export class SidebarBrand extends LitElement { } firstUpdated(): void { - Config.get().then((c) => (this.config = c)); + configureSentry().then((c) => {this.config = c;}); } render(): TemplateResult { return html`
- authentik icon + authentik icon
`; } diff --git a/web/src/elements/sidebar/SidebarUser.ts b/web/src/elements/sidebar/SidebarUser.ts index 01a75d470..fc17cfbde 100644 --- a/web/src/elements/sidebar/SidebarUser.ts +++ b/web/src/elements/sidebar/SidebarUser.ts @@ -5,10 +5,11 @@ import NavStyle from "@patternfly/patternfly/components/Nav/nav.css"; import fa from "@fortawesome/fontawesome-free/css/all.css"; // @ts-ignore import AvatarStyle from "@patternfly/patternfly/components/Avatar/avatar.css"; -import { User } from "../../api/Users"; +import { me } from "../../api/Users"; import { until } from "lit-html/directives/until"; import "../notifications/NotificationTrigger"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-sidebar-user") export class SidebarUser extends LitElement { @@ -37,8 +38,8 @@ export class SidebarUser extends LitElement { render(): TemplateResult { return html` - ${until(User.me().then((u) => { - return html``; + ${until(me().then((u) => { + return html``; }), html``)} diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index f6c4a2b22..9345966d9 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -5,7 +5,7 @@ import { COMMON_STYLES } from "../../common/styles"; import "./TablePagination"; import "../EmptyState"; - +import "../Spinner"; export class TableColumn { @@ -95,6 +95,12 @@ export abstract class Table extends LitElement { @property({type: String}) search?: string; + @property({type: Boolean}) + checkbox = false; + + @property({attribute: false}) + selectedElements: T[] = []; + @property({type: Boolean}) expandable = false; @@ -169,6 +175,21 @@ export abstract class Table extends LitElement { } return html` + ${this.checkbox ? html` + = 0} + @input=${(ev: InputEvent) => { + if ((ev.target as HTMLInputElement).checked) { + // Add item to selected + this.selectedElements.push(item); + } else { + // Get index of item and remove if selected + const index = this.selectedElements.indexOf(item); + if (index <= -1) return; + this.selectedElements.splice(index, 1); + } + }} /> + ` : html``} ${this.expandable ? html`