Compare commits
	
		
			228 Commits
		
	
	
		
			version/0.
			...
			version/20
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 94182f88a4 | |||
| 1c25f4f09b | |||
| 6495d6c50a | |||
| b81f3e4a38 | |||
| aad3b43ac3 | |||
| 60f52f102a | |||
| f3ccb5341d | |||
| cb73210447 | |||
| 81efc9a673 | |||
| 72c6c0da9b | |||
| 8fef839965 | |||
| 87b830ff9a | |||
| 8acb9dde5f | |||
| 36e8b1004c | |||
| f959212692 | |||
| 2d2a404028 | |||
| 394ad6ade5 | |||
| 4baf9e4a22 | |||
| d020599e09 | |||
| 4f28a89e63 | |||
| f8b4b92e8d | |||
| 33f208657c | |||
| c1fbfc63ab | |||
| 192dbe05c4 | |||
| 0b41cb84f0 | |||
| d637bd0bf9 | |||
| a2bddc6d91 | |||
| 2e42da11ea | |||
| f297d1256d | |||
| 5e1e5afb24 | |||
| da59e7c4a7 | |||
| 8684d106d5 | |||
| 2579e168c3 | |||
| 7f5caf901d | |||
| 1c686e19b5 | |||
| 3cc92f6c97 | |||
| 8f5b33a3a2 | |||
| 4447345345 | |||
| 42c6401ba7 | |||
| eef111bcfd | |||
| 6192b2787f | |||
| c7d28f8ca9 | |||
| 1342266368 | |||
| 7ff679b1a3 | |||
| 8beddcddb0 | |||
| 9fe8554f28 | |||
| 812fe72e60 | |||
| d0e4533cdd | |||
| b1b5d94ddc | |||
| 59722e0bbe | |||
| 9c5bb3998c | |||
| c180c4b1a2 | |||
| 308896719d | |||
| 95c1473dd2 | |||
| b14c5039ed | |||
| b6948334f2 | |||
| 29e08e7477 | |||
| 36bc1dc020 | |||
| 61d1407804 | |||
| 47ddf0d7f2 | |||
| cb36a3c8c7 | |||
| cac94792fa | |||
| 6f56c37d2f | |||
| 8369fa16ae | |||
| f30bdbecd6 | |||
| c727c845df | |||
| b2b737e59e | |||
| e2b930afe3 | |||
| 36c0b924bc | |||
| 1ccf6dcf6f | |||
| f8a426f0e8 | |||
| f8756d0fc9 | |||
| fd6d99f4f9 | |||
| 04379f2c90 | |||
| ba1195cf70 | |||
| b0bd9212c7 | |||
| 209179e012 | |||
| df16f635fa | |||
| 14ccf47a2b | |||
| 2aac024477 | |||
| 4743e72e18 | |||
| cab2942c4e | |||
| 9fb5ce2a1a | |||
| 0eab4489c5 | |||
| 3aae030b23 | |||
| e7060cb90a | |||
| 6c0b9e3525 | |||
| 82bb179bc2 | |||
| 774eb0388b | |||
| 6ed78830a0 | |||
| 6fe323f1a7 | |||
| 85c2db018e | |||
| bc9e7e8b93 | |||
| 08c58ce3fb | |||
| c3bc986473 | |||
| 2e69efe699 | |||
| 4daa373dcf | |||
| a85b8a65c0 | |||
| d8dc1f8bb5 | |||
| 0f4d5bc3b0 | |||
| 6eed549577 | |||
| be54ba4fe2 | |||
| 68b9c34f78 | |||
| 3584bdf530 | |||
| e712719333 | |||
| 9a21c2f6bd | |||
| 0632d8ff37 | |||
| 6bfaf71c12 | |||
| b6c8c319e5 | |||
| 4fde1b7365 | |||
| 412f5b9210 | |||
| a9e53cd52a | |||
| d0ee7908ab | |||
| e69834dec4 | |||
| 1b9d22615c | |||
| e995536a15 | |||
| e6818faab1 | |||
| 010e834149 | |||
| 16d5e1d9ff | |||
| 765ae80698 | |||
| bbd0ff24d8 | |||
| 7a403613b2 | |||
| 4ad184a3fb | |||
| 48d5f28e7a | |||
| 0cb48121b2 | |||
| 4194ffe2d4 | |||
| 4636fe7e64 | |||
| 182d714b16 | |||
| 540c22ce15 | |||
| 8c3008abce | |||
| 8a22c86aaa | |||
| 22ce142cb8 | |||
| 1a292feebb | |||
| 09f4d812b3 | |||
| 2bab4ebfe8 | |||
| a8647caca9 | |||
| 590597caf6 | |||
| 7b43777b22 | |||
| 77861b52e3 | |||
| 5f9c1e229c | |||
| 119adb3e7b | |||
| 5db38bd0b7 | |||
| 0e1587bc1a | |||
| dc16a8a4c9 | |||
| a6d0c8c26c | |||
| 5797a3743a | |||
| b7e43efb34 | |||
| 48df12d045 | |||
| 4fea0f5939 | |||
| a7bdd63e4d | |||
| e216efb6ec | |||
| 378fe38b12 | |||
| ce9fb8801c | |||
| 67ca83c228 | |||
| ee2e737782 | |||
| b04c9a2098 | |||
| 7f7b7e37c1 | |||
| e7c96eb70d | |||
| e8debce9c8 | |||
| bcd0686a33 | |||
| 55322995a1 | |||
| dff5eb69c8 | |||
| b747022bc1 | |||
| 885fcff495 | |||
| 5b18e28753 | |||
| 9848c5f3eb | |||
| fc98c3934a | |||
| 7964061466 | |||
| 5f90f54195 | |||
| 49eb568d3c | |||
| d47d9103c7 | |||
| 12cbe464fc | |||
| d17b2b0d1b | |||
| f17d809219 | |||
| 6c8e9fb553 | |||
| 43bb29e16a | |||
| 29edbb0357 | |||
| 12ae867759 | |||
| a20ca9136b | |||
| 3759e96e7d | |||
| 480d882a82 | |||
| e5e1e3737d | |||
| 8dddcf891e | |||
| 319104c39b | |||
| a9336f069c | |||
| 33f5169f36 | |||
| 4c690a20ef | |||
| f68c8f7d90 | |||
| 95b56a0005 | |||
| 811c569b54 | |||
| 3ac3a8eebe | |||
| 6a5a243dac | |||
| 3549a9ecdd | |||
| ee916a68a4 | |||
| e9ca42cbb9 | |||
| 692d577217 | |||
| f192ee5052 | |||
| c95f8e8418 | |||
| 9549a7188b | |||
| 4998ccbe41 | |||
| a56ddb2b8e | |||
| 3cc6b8ee38 | |||
| 927ab509a1 | |||
| c85506f43c | |||
| 4157a0780d | |||
| 79da2bf698 | |||
| c3e9168b46 | |||
| d16838bbed | |||
| 6032efb67d | |||
| 322c6f01c2 | |||
| 71a58955f2 | |||
| f035da440a | |||
| 001de38d85 | |||
| 3ea39fe122 | |||
| 7bfa217cae | |||
| fdb9b45c51 | |||
| 116375084c | |||
| 1fca1df9dc | |||
| 4464ecc060 | |||
| 1af4373d97 | |||
| 28bbf5ac7f | |||
| 23f61e6b4f | |||
| db135a6dbc | |||
| a4dc6d13b5 | |||
| 4d88dcff08 | |||
| 6a835ad192 | |||
| efc849e760 | |||
| e62333dfb3 | 
| @ -1,5 +1,5 @@ | ||||
| [bumpversion] | ||||
| current_version = 0.13.4-stable | ||||
| current_version = 2021.1.1-rc1 | ||||
| tag = True | ||||
| commit = True | ||||
| parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*) | ||||
| @ -31,6 +31,6 @@ values = | ||||
|  | ||||
| [bumpversion:file:authentik/__init__.py] | ||||
|  | ||||
| [bumpversion:file:proxy/pkg/version.go] | ||||
| [bumpversion:file:outpost/pkg/version.go] | ||||
|  | ||||
| [bumpversion:file:web/src/constants.ts] | ||||
|  | ||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @ -18,11 +18,11 @@ jobs: | ||||
|       - name: Building Docker Image | ||||
|         run: docker build | ||||
|           --no-cache | ||||
|           -t beryju/authentik:0.13.4-stable | ||||
|           -t beryju/authentik:2021.1.1-rc1 | ||||
|           -t beryju/authentik:latest | ||||
|           -f Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/authentik:0.13.4-stable | ||||
|         run: docker push beryju/authentik:2021.1.1-rc1 | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/authentik:latest | ||||
|   build-proxy: | ||||
| @ -48,11 +48,11 @@ jobs: | ||||
|           cd proxy/ | ||||
|           docker build \ | ||||
|           --no-cache \ | ||||
|           -t beryju/authentik-proxy:0.13.4-stable \ | ||||
|           -t beryju/authentik-proxy:2021.1.1-rc1 \ | ||||
|           -t beryju/authentik-proxy:latest \ | ||||
|           -f Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/authentik-proxy:0.13.4-stable | ||||
|         run: docker push beryju/authentik-proxy:2021.1.1-rc1 | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/authentik-proxy:latest | ||||
|   build-static: | ||||
| @ -69,11 +69,11 @@ jobs: | ||||
|           cd web/ | ||||
|           docker build \ | ||||
|           --no-cache \ | ||||
|           -t beryju/authentik-static:0.13.4-stable \ | ||||
|           -t beryju/authentik-static:2021.1.1-rc1 \ | ||||
|           -t beryju/authentik-static:latest \ | ||||
|           -f Dockerfile . | ||||
|       - name: Push Docker Container to Registry (versioned) | ||||
|         run: docker push beryju/authentik-static:0.13.4-stable | ||||
|         run: docker push beryju/authentik-static:2021.1.1-rc1 | ||||
|       - name: Push Docker Container to Registry (latest) | ||||
|         run: docker push beryju/authentik-static:latest | ||||
|   test-release: | ||||
| @ -107,5 +107,5 @@ jobs: | ||||
|           SENTRY_PROJECT: authentik | ||||
|           SENTRY_URL: https://sentry.beryju.org | ||||
|         with: | ||||
|           tagName: 0.13.4-stable | ||||
|           tagName: 2021.1.1-rc1 | ||||
|           environment: beryjuorg-prod | ||||
|  | ||||
							
								
								
									
										687
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										687
									
								
								LICENSE
									
									
									
									
									
								
							| @ -1,21 +1,674 @@ | ||||
| MIT License | ||||
|                     GNU GENERAL PUBLIC LICENSE | ||||
|                        Version 3, 29 June 2007 | ||||
|  | ||||
| Copyright (c) 2019 BeryJu.org | ||||
|  Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/> | ||||
|  Everyone is permitted to copy and distribute verbatim copies | ||||
|  of this license document, but changing it is not allowed. | ||||
|  | ||||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||||
| of this software and associated documentation files (the "Software"), to deal | ||||
| in the Software without restriction, including without limitation the rights | ||||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||||
| copies of the Software, and to permit persons to whom the Software is | ||||
| furnished to do so, subject to the following conditions: | ||||
|                             Preamble | ||||
|  | ||||
| The above copyright notice and this permission notice shall be included in all | ||||
| copies or substantial portions of the Software. | ||||
|   The GNU General Public License is a free, copyleft license for | ||||
| software and other kinds of works. | ||||
|  | ||||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||||
| SOFTWARE. | ||||
|   The licenses for most software and other practical works are designed | ||||
| to take away your freedom to share and change the works.  By contrast, | ||||
| the GNU General Public License is intended to guarantee your freedom to | ||||
| share and change all versions of a program--to make sure it remains free | ||||
| software for all its users.  We, the Free Software Foundation, use the | ||||
| GNU General Public License for most of our software; it applies also to | ||||
| any other work released this way by its authors.  You can apply it to | ||||
| your programs, too. | ||||
|  | ||||
|   When we speak of free software, we are referring to freedom, not | ||||
| price.  Our General Public Licenses are designed to make sure that you | ||||
| have the freedom to distribute copies of free software (and charge for | ||||
| them if you wish), that you receive source code or can get it if you | ||||
| want it, that you can change the software or use pieces of it in new | ||||
| free programs, and that you know you can do these things. | ||||
|  | ||||
|   To protect your rights, we need to prevent others from denying you | ||||
| these rights or asking you to surrender the rights.  Therefore, you have | ||||
| certain responsibilities if you distribute copies of the software, or if | ||||
| you modify it: responsibilities to respect the freedom of others. | ||||
|  | ||||
|   For example, if you distribute copies of such a program, whether | ||||
| gratis or for a fee, you must pass on to the recipients the same | ||||
| freedoms that you received.  You must make sure that they, too, receive | ||||
| or can get the source code.  And you must show them these terms so they | ||||
| know their rights. | ||||
|  | ||||
|   Developers that use the GNU GPL protect your rights with two steps: | ||||
| (1) assert copyright on the software, and (2) offer you this License | ||||
| giving you legal permission to copy, distribute and/or modify it. | ||||
|  | ||||
|   For the developers' and authors' protection, the GPL clearly explains | ||||
| that there is no warranty for this free software.  For both users' and | ||||
| authors' sake, the GPL requires that modified versions be marked as | ||||
| changed, so that their problems will not be attributed erroneously to | ||||
| authors of previous versions. | ||||
|  | ||||
|   Some devices are designed to deny users access to install or run | ||||
| modified versions of the software inside them, although the manufacturer | ||||
| can do so.  This is fundamentally incompatible with the aim of | ||||
| protecting users' freedom to change the software.  The systematic | ||||
| pattern of such abuse occurs in the area of products for individuals to | ||||
| use, which is precisely where it is most unacceptable.  Therefore, we | ||||
| have designed this version of the GPL to prohibit the practice for those | ||||
| products.  If such problems arise substantially in other domains, we | ||||
| stand ready to extend this provision to those domains in future versions | ||||
| of the GPL, as needed to protect the freedom of users. | ||||
|  | ||||
|   Finally, every program is threatened constantly by software patents. | ||||
| States should not allow patents to restrict development and use of | ||||
| software on general-purpose computers, but in those that do, we wish to | ||||
| avoid the special danger that patents applied to a free program could | ||||
| make it effectively proprietary.  To prevent this, the GPL assures that | ||||
| patents cannot be used to render the program non-free. | ||||
|  | ||||
|   The precise terms and conditions for copying, distribution and | ||||
| modification follow. | ||||
|  | ||||
|                        TERMS AND CONDITIONS | ||||
|  | ||||
|   0. Definitions. | ||||
|  | ||||
|   "This License" refers to version 3 of the GNU General Public License. | ||||
|  | ||||
|   "Copyright" also means copyright-like laws that apply to other kinds of | ||||
| works, such as semiconductor masks. | ||||
|  | ||||
|   "The Program" refers to any copyrightable work licensed under this | ||||
| License.  Each licensee is addressed as "you".  "Licensees" and | ||||
| "recipients" may be individuals or organizations. | ||||
|  | ||||
|   To "modify" a work means to copy from or adapt all or part of the work | ||||
| in a fashion requiring copyright permission, other than the making of an | ||||
| exact copy.  The resulting work is called a "modified version" of the | ||||
| earlier work or a work "based on" the earlier work. | ||||
|  | ||||
|   A "covered work" means either the unmodified Program or a work based | ||||
| on the Program. | ||||
|  | ||||
|   To "propagate" a work means to do anything with it that, without | ||||
| permission, would make you directly or secondarily liable for | ||||
| infringement under applicable copyright law, except executing it on a | ||||
| computer or modifying a private copy.  Propagation includes copying, | ||||
| distribution (with or without modification), making available to the | ||||
| public, and in some countries other activities as well. | ||||
|  | ||||
|   To "convey" a work means any kind of propagation that enables other | ||||
| parties to make or receive copies.  Mere interaction with a user through | ||||
| a computer network, with no transfer of a copy, is not conveying. | ||||
|  | ||||
|   An interactive user interface displays "Appropriate Legal Notices" | ||||
| to the extent that it includes a convenient and prominently visible | ||||
| feature that (1) displays an appropriate copyright notice, and (2) | ||||
| tells the user that there is no warranty for the work (except to the | ||||
| extent that warranties are provided), that licensees may convey the | ||||
| work under this License, and how to view a copy of this License.  If | ||||
| the interface presents a list of user commands or options, such as a | ||||
| menu, a prominent item in the list meets this criterion. | ||||
|  | ||||
|   1. Source Code. | ||||
|  | ||||
|   The "source code" for a work means the preferred form of the work | ||||
| for making modifications to it.  "Object code" means any non-source | ||||
| form of a work. | ||||
|  | ||||
|   A "Standard Interface" means an interface that either is an official | ||||
| standard defined by a recognized standards body, or, in the case of | ||||
| interfaces specified for a particular programming language, one that | ||||
| is widely used among developers working in that language. | ||||
|  | ||||
|   The "System Libraries" of an executable work include anything, other | ||||
| than the work as a whole, that (a) is included in the normal form of | ||||
| packaging a Major Component, but which is not part of that Major | ||||
| Component, and (b) serves only to enable use of the work with that | ||||
| Major Component, or to implement a Standard Interface for which an | ||||
| implementation is available to the public in source code form.  A | ||||
| "Major Component", in this context, means a major essential component | ||||
| (kernel, window system, and so on) of the specific operating system | ||||
| (if any) on which the executable work runs, or a compiler used to | ||||
| produce the work, or an object code interpreter used to run it. | ||||
|  | ||||
|   The "Corresponding Source" for a work in object code form means all | ||||
| the source code needed to generate, install, and (for an executable | ||||
| work) run the object code and to modify the work, including scripts to | ||||
| control those activities.  However, it does not include the work's | ||||
| System Libraries, or general-purpose tools or generally available free | ||||
| programs which are used unmodified in performing those activities but | ||||
| which are not part of the work.  For example, Corresponding Source | ||||
| includes interface definition files associated with source files for | ||||
| the work, and the source code for shared libraries and dynamically | ||||
| linked subprograms that the work is specifically designed to require, | ||||
| such as by intimate data communication or control flow between those | ||||
| subprograms and other parts of the work. | ||||
|  | ||||
|   The Corresponding Source need not include anything that users | ||||
| can regenerate automatically from other parts of the Corresponding | ||||
| Source. | ||||
|  | ||||
|   The Corresponding Source for a work in source code form is that | ||||
| same work. | ||||
|  | ||||
|   2. Basic Permissions. | ||||
|  | ||||
|   All rights granted under this License are granted for the term of | ||||
| copyright on the Program, and are irrevocable provided the stated | ||||
| conditions are met.  This License explicitly affirms your unlimited | ||||
| permission to run the unmodified Program.  The output from running a | ||||
| covered work is covered by this License only if the output, given its | ||||
| content, constitutes a covered work.  This License acknowledges your | ||||
| rights of fair use or other equivalent, as provided by copyright law. | ||||
|  | ||||
|   You may make, run and propagate covered works that you do not | ||||
| convey, without conditions so long as your license otherwise remains | ||||
| in force.  You may convey covered works to others for the sole purpose | ||||
| of having them make modifications exclusively for you, or provide you | ||||
| with facilities for running those works, provided that you comply with | ||||
| the terms of this License in conveying all material for which you do | ||||
| not control copyright.  Those thus making or running the covered works | ||||
| for you must do so exclusively on your behalf, under your direction | ||||
| and control, on terms that prohibit them from making any copies of | ||||
| your copyrighted material outside their relationship with you. | ||||
|  | ||||
|   Conveying under any other circumstances is permitted solely under | ||||
| the conditions stated below.  Sublicensing is not allowed; section 10 | ||||
| makes it unnecessary. | ||||
|  | ||||
|   3. Protecting Users' Legal Rights From Anti-Circumvention Law. | ||||
|  | ||||
|   No covered work shall be deemed part of an effective technological | ||||
| measure under any applicable law fulfilling obligations under article | ||||
| 11 of the WIPO copyright treaty adopted on 20 December 1996, or | ||||
| similar laws prohibiting or restricting circumvention of such | ||||
| measures. | ||||
|  | ||||
|   When you convey a covered work, you waive any legal power to forbid | ||||
| circumvention of technological measures to the extent such circumvention | ||||
| is effected by exercising rights under this License with respect to | ||||
| the covered work, and you disclaim any intention to limit operation or | ||||
| modification of the work as a means of enforcing, against the work's | ||||
| users, your or third parties' legal rights to forbid circumvention of | ||||
| technological measures. | ||||
|  | ||||
|   4. Conveying Verbatim Copies. | ||||
|  | ||||
|   You may convey verbatim copies of the Program's source code as you | ||||
| receive it, in any medium, provided that you conspicuously and | ||||
| appropriately publish on each copy an appropriate copyright notice; | ||||
| keep intact all notices stating that this License and any | ||||
| non-permissive terms added in accord with section 7 apply to the code; | ||||
| keep intact all notices of the absence of any warranty; and give all | ||||
| recipients a copy of this License along with the Program. | ||||
|  | ||||
|   You may charge any price or no price for each copy that you convey, | ||||
| and you may offer support or warranty protection for a fee. | ||||
|  | ||||
|   5. Conveying Modified Source Versions. | ||||
|  | ||||
|   You may convey a work based on the Program, or the modifications to | ||||
| produce it from the Program, in the form of source code under the | ||||
| terms of section 4, provided that you also meet all of these conditions: | ||||
|  | ||||
|     a) The work must carry prominent notices stating that you modified | ||||
|     it, and giving a relevant date. | ||||
|  | ||||
|     b) The work must carry prominent notices stating that it is | ||||
|     released under this License and any conditions added under section | ||||
|     7.  This requirement modifies the requirement in section 4 to | ||||
|     "keep intact all notices". | ||||
|  | ||||
|     c) You must license the entire work, as a whole, under this | ||||
|     License to anyone who comes into possession of a copy.  This | ||||
|     License will therefore apply, along with any applicable section 7 | ||||
|     additional terms, to the whole of the work, and all its parts, | ||||
|     regardless of how they are packaged.  This License gives no | ||||
|     permission to license the work in any other way, but it does not | ||||
|     invalidate such permission if you have separately received it. | ||||
|  | ||||
|     d) If the work has interactive user interfaces, each must display | ||||
|     Appropriate Legal Notices; however, if the Program has interactive | ||||
|     interfaces that do not display Appropriate Legal Notices, your | ||||
|     work need not make them do so. | ||||
|  | ||||
|   A compilation of a covered work with other separate and independent | ||||
| works, which are not by their nature extensions of the covered work, | ||||
| and which are not combined with it such as to form a larger program, | ||||
| in or on a volume of a storage or distribution medium, is called an | ||||
| "aggregate" if the compilation and its resulting copyright are not | ||||
| used to limit the access or legal rights of the compilation's users | ||||
| beyond what the individual works permit.  Inclusion of a covered work | ||||
| in an aggregate does not cause this License to apply to the other | ||||
| parts of the aggregate. | ||||
|  | ||||
|   6. Conveying Non-Source Forms. | ||||
|  | ||||
|   You may convey a covered work in object code form under the terms | ||||
| of sections 4 and 5, provided that you also convey the | ||||
| machine-readable Corresponding Source under the terms of this License, | ||||
| in one of these ways: | ||||
|  | ||||
|     a) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by the | ||||
|     Corresponding Source fixed on a durable physical medium | ||||
|     customarily used for software interchange. | ||||
|  | ||||
|     b) Convey the object code in, or embodied in, a physical product | ||||
|     (including a physical distribution medium), accompanied by a | ||||
|     written offer, valid for at least three years and valid for as | ||||
|     long as you offer spare parts or customer support for that product | ||||
|     model, to give anyone who possesses the object code either (1) a | ||||
|     copy of the Corresponding Source for all the software in the | ||||
|     product that is covered by this License, on a durable physical | ||||
|     medium customarily used for software interchange, for a price no | ||||
|     more than your reasonable cost of physically performing this | ||||
|     conveying of source, or (2) access to copy the | ||||
|     Corresponding Source from a network server at no charge. | ||||
|  | ||||
|     c) Convey individual copies of the object code with a copy of the | ||||
|     written offer to provide the Corresponding Source.  This | ||||
|     alternative is allowed only occasionally and noncommercially, and | ||||
|     only if you received the object code with such an offer, in accord | ||||
|     with subsection 6b. | ||||
|  | ||||
|     d) Convey the object code by offering access from a designated | ||||
|     place (gratis or for a charge), and offer equivalent access to the | ||||
|     Corresponding Source in the same way through the same place at no | ||||
|     further charge.  You need not require recipients to copy the | ||||
|     Corresponding Source along with the object code.  If the place to | ||||
|     copy the object code is a network server, the Corresponding Source | ||||
|     may be on a different server (operated by you or a third party) | ||||
|     that supports equivalent copying facilities, provided you maintain | ||||
|     clear directions next to the object code saying where to find the | ||||
|     Corresponding Source.  Regardless of what server hosts the | ||||
|     Corresponding Source, you remain obligated to ensure that it is | ||||
|     available for as long as needed to satisfy these requirements. | ||||
|  | ||||
|     e) Convey the object code using peer-to-peer transmission, provided | ||||
|     you inform other peers where the object code and Corresponding | ||||
|     Source of the work are being offered to the general public at no | ||||
|     charge under subsection 6d. | ||||
|  | ||||
|   A separable portion of the object code, whose source code is excluded | ||||
| from the Corresponding Source as a System Library, need not be | ||||
| included in conveying the object code work. | ||||
|  | ||||
|   A "User Product" is either (1) a "consumer product", which means any | ||||
| tangible personal property which is normally used for personal, family, | ||||
| or household purposes, or (2) anything designed or sold for incorporation | ||||
| into a dwelling.  In determining whether a product is a consumer product, | ||||
| doubtful cases shall be resolved in favor of coverage.  For a particular | ||||
| product received by a particular user, "normally used" refers to a | ||||
| typical or common use of that class of product, regardless of the status | ||||
| of the particular user or of the way in which the particular user | ||||
| actually uses, or expects or is expected to use, the product.  A product | ||||
| is a consumer product regardless of whether the product has substantial | ||||
| commercial, industrial or non-consumer uses, unless such uses represent | ||||
| the only significant mode of use of the product. | ||||
|  | ||||
|   "Installation Information" for a User Product means any methods, | ||||
| procedures, authorization keys, or other information required to install | ||||
| and execute modified versions of a covered work in that User Product from | ||||
| a modified version of its Corresponding Source.  The information must | ||||
| suffice to ensure that the continued functioning of the modified object | ||||
| code is in no case prevented or interfered with solely because | ||||
| modification has been made. | ||||
|  | ||||
|   If you convey an object code work under this section in, or with, or | ||||
| specifically for use in, a User Product, and the conveying occurs as | ||||
| part of a transaction in which the right of possession and use of the | ||||
| User Product is transferred to the recipient in perpetuity or for a | ||||
| fixed term (regardless of how the transaction is characterized), the | ||||
| Corresponding Source conveyed under this section must be accompanied | ||||
| by the Installation Information.  But this requirement does not apply | ||||
| if neither you nor any third party retains the ability to install | ||||
| modified object code on the User Product (for example, the work has | ||||
| been installed in ROM). | ||||
|  | ||||
|   The requirement to provide Installation Information does not include a | ||||
| requirement to continue to provide support service, warranty, or updates | ||||
| for a work that has been modified or installed by the recipient, or for | ||||
| the User Product in which it has been modified or installed.  Access to a | ||||
| network may be denied when the modification itself materially and | ||||
| adversely affects the operation of the network or violates the rules and | ||||
| protocols for communication across the network. | ||||
|  | ||||
|   Corresponding Source conveyed, and Installation Information provided, | ||||
| in accord with this section must be in a format that is publicly | ||||
| documented (and with an implementation available to the public in | ||||
| source code form), and must require no special password or key for | ||||
| unpacking, reading or copying. | ||||
|  | ||||
|   7. Additional Terms. | ||||
|  | ||||
|   "Additional permissions" are terms that supplement the terms of this | ||||
| License by making exceptions from one or more of its conditions. | ||||
| Additional permissions that are applicable to the entire Program shall | ||||
| be treated as though they were included in this License, to the extent | ||||
| that they are valid under applicable law.  If additional permissions | ||||
| apply only to part of the Program, that part may be used separately | ||||
| under those permissions, but the entire Program remains governed by | ||||
| this License without regard to the additional permissions. | ||||
|  | ||||
|   When you convey a copy of a covered work, you may at your option | ||||
| remove any additional permissions from that copy, or from any part of | ||||
| it.  (Additional permissions may be written to require their own | ||||
| removal in certain cases when you modify the work.)  You may place | ||||
| additional permissions on material, added by you to a covered work, | ||||
| for which you have or can give appropriate copyright permission. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, for material you | ||||
| add to a covered work, you may (if authorized by the copyright holders of | ||||
| that material) supplement the terms of this License with terms: | ||||
|  | ||||
|     a) Disclaiming warranty or limiting liability differently from the | ||||
|     terms of sections 15 and 16 of this License; or | ||||
|  | ||||
|     b) Requiring preservation of specified reasonable legal notices or | ||||
|     author attributions in that material or in the Appropriate Legal | ||||
|     Notices displayed by works containing it; or | ||||
|  | ||||
|     c) Prohibiting misrepresentation of the origin of that material, or | ||||
|     requiring that modified versions of such material be marked in | ||||
|     reasonable ways as different from the original version; or | ||||
|  | ||||
|     d) Limiting the use for publicity purposes of names of licensors or | ||||
|     authors of the material; or | ||||
|  | ||||
|     e) Declining to grant rights under trademark law for use of some | ||||
|     trade names, trademarks, or service marks; or | ||||
|  | ||||
|     f) Requiring indemnification of licensors and authors of that | ||||
|     material by anyone who conveys the material (or modified versions of | ||||
|     it) with contractual assumptions of liability to the recipient, for | ||||
|     any liability that these contractual assumptions directly impose on | ||||
|     those licensors and authors. | ||||
|  | ||||
|   All other non-permissive additional terms are considered "further | ||||
| restrictions" within the meaning of section 10.  If the Program as you | ||||
| received it, or any part of it, contains a notice stating that it is | ||||
| governed by this License along with a term that is a further | ||||
| restriction, you may remove that term.  If a license document contains | ||||
| a further restriction but permits relicensing or conveying under this | ||||
| License, you may add to a covered work material governed by the terms | ||||
| of that license document, provided that the further restriction does | ||||
| not survive such relicensing or conveying. | ||||
|  | ||||
|   If you add terms to a covered work in accord with this section, you | ||||
| must place, in the relevant source files, a statement of the | ||||
| additional terms that apply to those files, or a notice indicating | ||||
| where to find the applicable terms. | ||||
|  | ||||
|   Additional terms, permissive or non-permissive, may be stated in the | ||||
| form of a separately written license, or stated as exceptions; | ||||
| the above requirements apply either way. | ||||
|  | ||||
|   8. Termination. | ||||
|  | ||||
|   You may not propagate or modify a covered work except as expressly | ||||
| provided under this License.  Any attempt otherwise to propagate or | ||||
| modify it is void, and will automatically terminate your rights under | ||||
| this License (including any patent licenses granted under the third | ||||
| paragraph of section 11). | ||||
|  | ||||
|   However, if you cease all violation of this License, then your | ||||
| license from a particular copyright holder is reinstated (a) | ||||
| provisionally, unless and until the copyright holder explicitly and | ||||
| finally terminates your license, and (b) permanently, if the copyright | ||||
| holder fails to notify you of the violation by some reasonable means | ||||
| prior to 60 days after the cessation. | ||||
|  | ||||
|   Moreover, your license from a particular copyright holder is | ||||
| reinstated permanently if the copyright holder notifies you of the | ||||
| violation by some reasonable means, this is the first time you have | ||||
| received notice of violation of this License (for any work) from that | ||||
| copyright holder, and you cure the violation prior to 30 days after | ||||
| your receipt of the notice. | ||||
|  | ||||
|   Termination of your rights under this section does not terminate the | ||||
| licenses of parties who have received copies or rights from you under | ||||
| this License.  If your rights have been terminated and not permanently | ||||
| reinstated, you do not qualify to receive new licenses for the same | ||||
| material under section 10. | ||||
|  | ||||
|   9. Acceptance Not Required for Having Copies. | ||||
|  | ||||
|   You are not required to accept this License in order to receive or | ||||
| run a copy of the Program.  Ancillary propagation of a covered work | ||||
| occurring solely as a consequence of using peer-to-peer transmission | ||||
| to receive a copy likewise does not require acceptance.  However, | ||||
| nothing other than this License grants you permission to propagate or | ||||
| modify any covered work.  These actions infringe copyright if you do | ||||
| not accept this License.  Therefore, by modifying or propagating a | ||||
| covered work, you indicate your acceptance of this License to do so. | ||||
|  | ||||
|   10. Automatic Licensing of Downstream Recipients. | ||||
|  | ||||
|   Each time you convey a covered work, the recipient automatically | ||||
| receives a license from the original licensors, to run, modify and | ||||
| propagate that work, subject to this License.  You are not responsible | ||||
| for enforcing compliance by third parties with this License. | ||||
|  | ||||
|   An "entity transaction" is a transaction transferring control of an | ||||
| organization, or substantially all assets of one, or subdividing an | ||||
| organization, or merging organizations.  If propagation of a covered | ||||
| work results from an entity transaction, each party to that | ||||
| transaction who receives a copy of the work also receives whatever | ||||
| licenses to the work the party's predecessor in interest had or could | ||||
| give under the previous paragraph, plus a right to possession of the | ||||
| Corresponding Source of the work from the predecessor in interest, if | ||||
| the predecessor has it or can get it with reasonable efforts. | ||||
|  | ||||
|   You may not impose any further restrictions on the exercise of the | ||||
| rights granted or affirmed under this License.  For example, you may | ||||
| not impose a license fee, royalty, or other charge for exercise of | ||||
| rights granted under this License, and you may not initiate litigation | ||||
| (including a cross-claim or counterclaim in a lawsuit) alleging that | ||||
| any patent claim is infringed by making, using, selling, offering for | ||||
| sale, or importing the Program or any portion of it. | ||||
|  | ||||
|   11. Patents. | ||||
|  | ||||
|   A "contributor" is a copyright holder who authorizes use under this | ||||
| License of the Program or a work on which the Program is based.  The | ||||
| work thus licensed is called the contributor's "contributor version". | ||||
|  | ||||
|   A contributor's "essential patent claims" are all patent claims | ||||
| owned or controlled by the contributor, whether already acquired or | ||||
| hereafter acquired, that would be infringed by some manner, permitted | ||||
| by this License, of making, using, or selling its contributor version, | ||||
| but do not include claims that would be infringed only as a | ||||
| consequence of further modification of the contributor version.  For | ||||
| purposes of this definition, "control" includes the right to grant | ||||
| patent sublicenses in a manner consistent with the requirements of | ||||
| this License. | ||||
|  | ||||
|   Each contributor grants you a non-exclusive, worldwide, royalty-free | ||||
| patent license under the contributor's essential patent claims, to | ||||
| make, use, sell, offer for sale, import and otherwise run, modify and | ||||
| propagate the contents of its contributor version. | ||||
|  | ||||
|   In the following three paragraphs, a "patent license" is any express | ||||
| agreement or commitment, however denominated, not to enforce a patent | ||||
| (such as an express permission to practice a patent or covenant not to | ||||
| sue for patent infringement).  To "grant" such a patent license to a | ||||
| party means to make such an agreement or commitment not to enforce a | ||||
| patent against the party. | ||||
|  | ||||
|   If you convey a covered work, knowingly relying on a patent license, | ||||
| and the Corresponding Source of the work is not available for anyone | ||||
| to copy, free of charge and under the terms of this License, through a | ||||
| publicly available network server or other readily accessible means, | ||||
| then you must either (1) cause the Corresponding Source to be so | ||||
| available, or (2) arrange to deprive yourself of the benefit of the | ||||
| patent license for this particular work, or (3) arrange, in a manner | ||||
| consistent with the requirements of this License, to extend the patent | ||||
| license to downstream recipients.  "Knowingly relying" means you have | ||||
| actual knowledge that, but for the patent license, your conveying the | ||||
| covered work in a country, or your recipient's use of the covered work | ||||
| in a country, would infringe one or more identifiable patents in that | ||||
| country that you have reason to believe are valid. | ||||
|  | ||||
|   If, pursuant to or in connection with a single transaction or | ||||
| arrangement, you convey, or propagate by procuring conveyance of, a | ||||
| covered work, and grant a patent license to some of the parties | ||||
| receiving the covered work authorizing them to use, propagate, modify | ||||
| or convey a specific copy of the covered work, then the patent license | ||||
| you grant is automatically extended to all recipients of the covered | ||||
| work and works based on it. | ||||
|  | ||||
|   A patent license is "discriminatory" if it does not include within | ||||
| the scope of its coverage, prohibits the exercise of, or is | ||||
| conditioned on the non-exercise of one or more of the rights that are | ||||
| specifically granted under this License.  You may not convey a covered | ||||
| work if you are a party to an arrangement with a third party that is | ||||
| in the business of distributing software, under which you make payment | ||||
| to the third party based on the extent of your activity of conveying | ||||
| the work, and under which the third party grants, to any of the | ||||
| parties who would receive the covered work from you, a discriminatory | ||||
| patent license (a) in connection with copies of the covered work | ||||
| conveyed by you (or copies made from those copies), or (b) primarily | ||||
| for and in connection with specific products or compilations that | ||||
| contain the covered work, unless you entered into that arrangement, | ||||
| or that patent license was granted, prior to 28 March 2007. | ||||
|  | ||||
|   Nothing in this License shall be construed as excluding or limiting | ||||
| any implied license or other defenses to infringement that may | ||||
| otherwise be available to you under applicable patent law. | ||||
|  | ||||
|   12. No Surrender of Others' Freedom. | ||||
|  | ||||
|   If conditions are imposed on you (whether by court order, agreement or | ||||
| otherwise) that contradict the conditions of this License, they do not | ||||
| excuse you from the conditions of this License.  If you cannot convey a | ||||
| covered work so as to satisfy simultaneously your obligations under this | ||||
| License and any other pertinent obligations, then as a consequence you may | ||||
| not convey it at all.  For example, if you agree to terms that obligate you | ||||
| to collect a royalty for further conveying from those to whom you convey | ||||
| the Program, the only way you could satisfy both those terms and this | ||||
| License would be to refrain entirely from conveying the Program. | ||||
|  | ||||
|   13. Use with the GNU Affero General Public License. | ||||
|  | ||||
|   Notwithstanding any other provision of this License, you have | ||||
| permission to link or combine any covered work with a work licensed | ||||
| under version 3 of the GNU Affero General Public License into a single | ||||
| combined work, and to convey the resulting work.  The terms of this | ||||
| License will continue to apply to the part which is the covered work, | ||||
| but the special requirements of the GNU Affero General Public License, | ||||
| section 13, concerning interaction through a network will apply to the | ||||
| combination as such. | ||||
|  | ||||
|   14. Revised Versions of this License. | ||||
|  | ||||
|   The Free Software Foundation may publish revised and/or new versions of | ||||
| the GNU General Public License from time to time.  Such new versions will | ||||
| be similar in spirit to the present version, but may differ in detail to | ||||
| address new problems or concerns. | ||||
|  | ||||
|   Each version is given a distinguishing version number.  If the | ||||
| Program specifies that a certain numbered version of the GNU General | ||||
| Public License "or any later version" applies to it, you have the | ||||
| option of following the terms and conditions either of that numbered | ||||
| version or of any later version published by the Free Software | ||||
| Foundation.  If the Program does not specify a version number of the | ||||
| GNU General Public License, you may choose any version ever published | ||||
| by the Free Software Foundation. | ||||
|  | ||||
|   If the Program specifies that a proxy can decide which future | ||||
| versions of the GNU General Public License can be used, that proxy's | ||||
| public statement of acceptance of a version permanently authorizes you | ||||
| to choose that version for the Program. | ||||
|  | ||||
|   Later license versions may give you additional or different | ||||
| permissions.  However, no additional obligations are imposed on any | ||||
| author or copyright holder as a result of your choosing to follow a | ||||
| later version. | ||||
|  | ||||
|   15. Disclaimer of Warranty. | ||||
|  | ||||
|   THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY | ||||
| APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT | ||||
| HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY | ||||
| OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, | ||||
| THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | ||||
| PURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM | ||||
| IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF | ||||
| ALL NECESSARY SERVICING, REPAIR OR CORRECTION. | ||||
|  | ||||
|   16. Limitation of Liability. | ||||
|  | ||||
|   IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING | ||||
| WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS | ||||
| THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY | ||||
| GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE | ||||
| USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF | ||||
| DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD | ||||
| PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), | ||||
| EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF | ||||
| SUCH DAMAGES. | ||||
|  | ||||
|   17. Interpretation of Sections 15 and 16. | ||||
|  | ||||
|   If the disclaimer of warranty and limitation of liability provided | ||||
| above cannot be given local legal effect according to their terms, | ||||
| reviewing courts shall apply local law that most closely approximates | ||||
| an absolute waiver of all civil liability in connection with the | ||||
| Program, unless a warranty or assumption of liability accompanies a | ||||
| copy of the Program in return for a fee. | ||||
|  | ||||
|                      END OF TERMS AND CONDITIONS | ||||
|  | ||||
|             How to Apply These Terms to Your New Programs | ||||
|  | ||||
|   If you develop a new program, and you want it to be of the greatest | ||||
| possible use to the public, the best way to achieve this is to make it | ||||
| free software which everyone can redistribute and change under these terms. | ||||
|  | ||||
|   To do so, attach the following notices to the program.  It is safest | ||||
| to attach them to the start of each source file to most effectively | ||||
| state the exclusion of warranty; and each file should have at least | ||||
| the "copyright" line and a pointer to where the full notice is found. | ||||
|  | ||||
|     <one line to give the program's name and a brief idea of what it does.> | ||||
|     Copyright (C) <year>  <name of author> | ||||
|  | ||||
|     This program is free software: you can redistribute it and/or modify | ||||
|     it under the terms of the GNU General Public License as published by | ||||
|     the Free Software Foundation, either version 3 of the License, or | ||||
|     (at your option) any later version. | ||||
|  | ||||
|     This program is distributed in the hope that it will be useful, | ||||
|     but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
|     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
|     GNU General Public License for more details. | ||||
|  | ||||
|     You should have received a copy of the GNU General Public License | ||||
|     along with this program.  If not, see <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| Also add information on how to contact you by electronic and paper mail. | ||||
|  | ||||
|   If the program does terminal interaction, make it output a short | ||||
| notice like this when it starts in an interactive mode: | ||||
|  | ||||
|     <program>  Copyright (C) <year>  <name of author> | ||||
|     This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. | ||||
|     This is free software, and you are welcome to redistribute it | ||||
|     under certain conditions; type `show c' for details. | ||||
|  | ||||
| The hypothetical commands `show w' and `show c' should show the appropriate | ||||
| parts of the General Public License.  Of course, your program's commands | ||||
| might be different; for a GUI interface, you would use an "about box". | ||||
|  | ||||
|   You should also get your employer (if you work as a programmer) or school, | ||||
| if any, to sign a "copyright disclaimer" for the program, if necessary. | ||||
| For more information on this, and how to apply and follow the GNU GPL, see | ||||
| <https://www.gnu.org/licenses/>. | ||||
|  | ||||
|   The GNU General Public License does not permit incorporating your program | ||||
| into proprietary programs.  If your program is a subroutine library, you | ||||
| may consider it more useful to permit linking proprietary applications with | ||||
| the library.  If this is what you want to do, use the GNU Lesser General | ||||
| Public License instead of this License.  But first, please read | ||||
| <https://www.gnu.org/licenses/why-not-lgpl.html>. | ||||
|  | ||||
							
								
								
									
										332
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										332
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							| @ -53,10 +53,10 @@ | ||||
|         }, | ||||
|         "autobahn": { | ||||
|             "hashes": [ | ||||
|                 "sha256:491238c31f78721eaa9d0593909ab455a4ea68127aadd76ecf67185143f5f298", | ||||
|                 "sha256:72b68a1ce1e10e3cbcc3b280aae86d5b2e7a1f409febab1ab91a8a3274113f6e" | ||||
|                 "sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895", | ||||
|                 "sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049" | ||||
|             ], | ||||
|             "version": "==20.12.2" | ||||
|             "version": "==20.12.3" | ||||
|         }, | ||||
|         "automat": { | ||||
|             "hashes": [ | ||||
| @ -74,18 +74,18 @@ | ||||
|         }, | ||||
|         "boto3": { | ||||
|             "hashes": [ | ||||
|                 "sha256:a05614300fd404c7952a55ae92e106b9400ae65886425aaab3104527be833848", | ||||
|                 "sha256:c7556b0861d982b71043fbc0df024644320c817ad796391c442d0c2f15a77223" | ||||
|                 "sha256:b5052144034e490358c659d0e480c17a4e604fd3aee9a97ddfe6e361a245a4a5", | ||||
|                 "sha256:efd6c96c98900e9fbf217f13cb58f59b793e51f69a1ce61817eefd31f17c6ef5" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.16.39" | ||||
|             "version": "==1.16.55" | ||||
|         }, | ||||
|         "botocore": { | ||||
|             "hashes": [ | ||||
|                 "sha256:449e4196160ff58ee27d2a626a7ce4cfff2640fe1806d7a279e73a30ad286347", | ||||
|                 "sha256:e0d0386098a072abd7b6c087e6149d997377c969a823ebe01b3f5bfabe9bfac0" | ||||
|                 "sha256:760d0c16c1474c2a46e3fa45e33ae7457b5cab7410737ab1692340ade764cc73", | ||||
|                 "sha256:b34327d84b3bb5620fb54603677a9a973b167290c2c1e7ab69c4a46b201c6d46" | ||||
|             ], | ||||
|             "version": "==1.19.39" | ||||
|             "version": "==1.19.55" | ||||
|         }, | ||||
|         "cachetools": { | ||||
|             "hashes": [ | ||||
| @ -152,11 +152,11 @@ | ||||
|         }, | ||||
|         "channels": { | ||||
|             "hashes": [ | ||||
|                 "sha256:74db79c9eca616be69d38013b22083ab5d3f9ccda1ab5e69096b1bb7da2d9b18", | ||||
|                 "sha256:f50a6e79757a64c1e45e95e144a2ac5f1e99ee44a0718ab182c501f5e5abd268" | ||||
|                 "sha256:056b72e51080a517a0f33a0a30003e03833b551d75394d6636c885d4edb8188f", | ||||
|                 "sha256:3f15bdd2138bb4796e76ea588a0a344b12a7964ea9b2e456f992fddb988a4317" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.0.2" | ||||
|             "version": "==3.0.3" | ||||
|         }, | ||||
|         "channels-redis": { | ||||
|             "hashes": [ | ||||
| @ -265,11 +265,11 @@ | ||||
|         }, | ||||
|         "django": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2", | ||||
|                 "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03" | ||||
|                 "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7", | ||||
|                 "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.1.4" | ||||
|             "version": "==3.1.5" | ||||
|         }, | ||||
|         "django-cors-middleware": { | ||||
|             "hashes": [ | ||||
| @ -343,15 +343,16 @@ | ||||
|         }, | ||||
|         "django-storages": { | ||||
|             "hashes": [ | ||||
|                 "sha256:056ec3e9e2b0c6f363913976072ffba2923e79e4859578047da139ba1637497e", | ||||
|                 "sha256:7af56611c62a1c174aab4e862efb7fdd98296dccf76f42135f5b6851fc313c97" | ||||
|                 "sha256:c823dbf56c9e35b0999a13d7e05062b837bae36c518a40255d522fbe3750fbb4", | ||||
|                 "sha256:f28765826d507a0309cfaa849bd084894bc71d81bf0d09479168d44785396f80" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==1.11" | ||||
|             "version": "==1.11.1" | ||||
|         }, | ||||
|         "djangorestframework": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7" | ||||
|                 "sha256:0209bafcb7b5010fdfec784034f059d512256424de2a0f084cb82b096d6dd6a7", | ||||
|                 "sha256:0898182b4737a7b584a2c73735d89816343369f259fea932d90dc78e35d8ac33" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.12.2" | ||||
| @ -366,11 +367,11 @@ | ||||
|         }, | ||||
|         "docker": { | ||||
|             "hashes": [ | ||||
|                 "sha256:317e95a48c32de8c1aac92a48066a5b73e218ed096e03758bcdd799a7130a1a1", | ||||
|                 "sha256:cffc771d4ea1389fc66bc95cb72d304aa41d1a1563482a9a000fba3a84ed5071" | ||||
|                 "sha256:0604a74719d5d2de438753934b755bfcda6f62f49b8e4b30969a4b0a2a8a1220", | ||||
|                 "sha256:e455fa49aabd4f22da9f4e1c1f9d16308286adc60abaf64bf3e1feafaed81d06" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==4.4.0" | ||||
|             "version": "==4.4.1" | ||||
|         }, | ||||
|         "drf-yasg2": { | ||||
|             "hashes": [ | ||||
| @ -411,10 +412,10 @@ | ||||
|         }, | ||||
|         "h11": { | ||||
|             "hashes": [ | ||||
|                 "sha256:3c6c61d69c6f13d41f1b80ab0322f1872702a3ba26e12aa864c928f6a43fbaab", | ||||
|                 "sha256:ab6c335e1b6ef34b205d5ca3e228c9299cc7218b049819ec84a388c2525e5d87" | ||||
|                 "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6", | ||||
|                 "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042" | ||||
|             ], | ||||
|             "version": "==0.11.0" | ||||
|             "version": "==0.12.0" | ||||
|         }, | ||||
|         "hiredis": { | ||||
|             "hashes": [ | ||||
| @ -486,10 +487,10 @@ | ||||
|         }, | ||||
|         "hyperlink": { | ||||
|             "hashes": [ | ||||
|                 "sha256:47fcc7cd339c6cb2444463ec3277bdcfe142c8b1daf2160bdd52248deec815af", | ||||
|                 "sha256:c528d405766f15a2c536230de7e160b65a08e20264d8891b3eb03307b0df3c63" | ||||
|                 "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", | ||||
|                 "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4" | ||||
|             ], | ||||
|             "version": "==20.0.1" | ||||
|             "version": "==21.0.0" | ||||
|         }, | ||||
|         "idna": { | ||||
|             "hashes": [ | ||||
| @ -646,46 +647,36 @@ | ||||
|         }, | ||||
|         "msgpack": { | ||||
|             "hashes": [ | ||||
|                 "sha256:01835e300967e5ad6fdbfc36eafe74df67ff47e16e0d6dee8766630550315903", | ||||
|                 "sha256:03c5554315317d76c25a15569dd52ac6047b105df71e861f24faf9675672b72d", | ||||
|                 "sha256:0968b368a9a9081435bfcb7a57a1e8b75c7bf038ef911b369acd2e038c7f873a", | ||||
|                 "sha256:1d7ab166401f7789bf11262439336c0a01b878f0d602e48f35c35d2e3a555820", | ||||
|                 "sha256:1e8d27bac821f8aa909904a704a67e5e8bc2e42b153415fc3621b7afbc06702b", | ||||
|                 "sha256:1fc9f21da9fd77088ebfd3c9941b044ca3f4a048e85f7ff5727f26bcdbffed61", | ||||
|                 "sha256:20196229acc193939223118c7420838749d5b0cece49cd397739a3a6ffcfe2d1", | ||||
|                 "sha256:2933443313289725f16bd7b99a8c3aa6a2cca1549e661d7407f056a0af80bf7b", | ||||
|                 "sha256:2966b155356fd231fa441131d7301e1596ee38974ad56dc57fd752fdbe2bb63f", | ||||
|                 "sha256:29a6fb3729215b6fcab786ef4f460a5406a5c056f7021191f70ff7712a3f6ba4", | ||||
|                 "sha256:35cbefa7d7bddfb4b0770a1b9ff721cd8dfe9a680947a68457974d5e3e6acc2f", | ||||
|                 "sha256:35ff1ac162a77fb78be360d9f771d36cbf1202e94fc6d70e284ad5db6ab72608", | ||||
|                 "sha256:40dd1ac7420f071e96b3e4a4a7b8e69546a6f8065ff5995dbacf53f86207eb98", | ||||
|                 "sha256:4bea1938e484c9caca9585105f447d6807c496c153b7244fa726b3cc4a68ec9e", | ||||
|                 "sha256:4e58b9f4a99bc3a90859bb006ec4422448a5ce39e5cd6e7498c56de5dcec9c34", | ||||
|                 "sha256:66d47e952856bfcde46d8351380d0b5b928a73112b66bc06d5367dfcc077c06a", | ||||
|                 "sha256:69f6aa503378548ea1e760c11aeb6fc91952bf3634fd806a38a0e47edb507fcd", | ||||
|                 "sha256:7033215267a0e9f60f4a5e4fb2228a932c404f237817caff8dc3115d9e7cd975", | ||||
|                 "sha256:7b50afd767cc053ad92fad39947c3670db27305fd1c49acded44d9d9ac8b56fd", | ||||
|                 "sha256:99ea9e65876546743b2b8bb5bc7adefbb03b9da78a899827467da197a48f790b", | ||||
|                 "sha256:abcc62303ac4d789878d4aac4cdba1bbe2adb478d67be99cd4a6d56ac3a4028f", | ||||
|                 "sha256:b107f9b36665bf7d7c6176a938a361a7aba16aa179d833919448f77287866484", | ||||
|                 "sha256:b5b27923b6c98a2616b7e906a29e4e10e1b4424aea87a0e0d5636327dc6ea315", | ||||
|                 "sha256:bf8eedc7bfbf63cbc9abe58287c32d78780a347835e82c23033c68f11f80bb05", | ||||
|                 "sha256:c144ff4954a6ea40aa603600c8be175349588fc68696092889fa34ab6e055060", | ||||
|                 "sha256:c4e5f96a1d0d916ce7a16decb7499e8923ddef007cf7d68412fb68767766648a", | ||||
|                 "sha256:c60e8b2bf754b8dcc1075c5bee0b177ed9193e7cbd2377faaf507120a948e697", | ||||
|                 "sha256:c82fc6cdba5737eb6ed0c926a30a5d56e7b050297375a16d6c5ad89b576fd979", | ||||
|                 "sha256:ce4ebe2c79411cd5671b20862831880e7850a2de699cff6626f48853fde61ae6", | ||||
|                 "sha256:d113c6b1239c62669ef3063693842605a3edbfebc39a333cf91ba60d314afe6d", | ||||
|                 "sha256:d3cea07ad16919a44e8d1ea67efa5244855cdce807d672f41694acc24d08834e", | ||||
|                 "sha256:d76672602db16e3f44bc1a85c7ee5f15d79e02fcf5bc9d133c2954753be6eddc", | ||||
|                 "sha256:decf2091b75987ca2564e3b742f9614eb7d57e39ff04eaa68af7a3fc5648f7ed", | ||||
|                 "sha256:e13b9007af66a3f62574bc0a13843df0e4402f5ee4b00a02aa1803f01d26b9fb", | ||||
|                 "sha256:e157edf4213dacafb0f862e0b7a3a18448250cec91aa1334f432f49028acc650", | ||||
|                 "sha256:e234ff83628ca3ab345bf97fb36ccbf6d2f1700f5e08868643bf4489edc960f8", | ||||
|                 "sha256:f08d9dd3ce0c5e972dc4653f0fb66d2703941e65356388c13032b578dd718261", | ||||
|                 "sha256:f20d7d4f1f0728560408ba6933154abccf0c20f24642a2404b43d5c23e4119ab" | ||||
|                 "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9", | ||||
|                 "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841", | ||||
|                 "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439", | ||||
|                 "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694", | ||||
|                 "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a", | ||||
|                 "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f", | ||||
|                 "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e", | ||||
|                 "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1", | ||||
|                 "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c", | ||||
|                 "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b", | ||||
|                 "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759", | ||||
|                 "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326", | ||||
|                 "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc", | ||||
|                 "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192", | ||||
|                 "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83", | ||||
|                 "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06", | ||||
|                 "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e", | ||||
|                 "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9", | ||||
|                 "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33", | ||||
|                 "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54", | ||||
|                 "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f", | ||||
|                 "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887", | ||||
|                 "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009", | ||||
|                 "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2", | ||||
|                 "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c", | ||||
|                 "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87", | ||||
|                 "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984", | ||||
|                 "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6" | ||||
|             ], | ||||
|             "version": "==1.0.1" | ||||
|             "version": "==1.0.2" | ||||
|         }, | ||||
|         "oauthlib": { | ||||
|             "hashes": [ | ||||
| @ -711,10 +702,10 @@ | ||||
|         }, | ||||
|         "prompt-toolkit": { | ||||
|             "hashes": [ | ||||
|                 "sha256:25c95d2ac813909f813c93fde734b6e44406d1477a9faef7c915ff37d39c0a8c", | ||||
|                 "sha256:7debb9a521e0b1ee7d2fe96ee4bd60ef03c6492784de0547337ca4433e46aa63" | ||||
|                 "sha256:ac329c69bd8564cb491940511957312c7b8959bb5b3cf3582b406068a51d5bb7", | ||||
|                 "sha256:b8b3d0bde65da350290c46a8f54f336b3cbf5464a4ac11239668d986852e79d5" | ||||
|             ], | ||||
|             "version": "==3.0.8" | ||||
|             "version": "==3.0.10" | ||||
|         }, | ||||
|         "psycopg2-binary": { | ||||
|             "hashes": [ | ||||
| @ -909,10 +900,10 @@ | ||||
|         }, | ||||
|         "pytz": { | ||||
|             "hashes": [ | ||||
|                 "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", | ||||
|                 "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" | ||||
|                 "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", | ||||
|                 "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" | ||||
|             ], | ||||
|             "version": "==2020.4" | ||||
|             "version": "==2020.5" | ||||
|         }, | ||||
|         "pyyaml": { | ||||
|             "hashes": [ | ||||
| @ -965,11 +956,11 @@ | ||||
|         }, | ||||
|         "rsa": { | ||||
|             "hashes": [ | ||||
|                 "sha256:109ea5a66744dd859bf16fe904b8d8b627adafb9408753161e766a92e7d681fa", | ||||
|                 "sha256:6166864e23d6b5195a5cfed6cd9fed0fe774e226d8f854fcb23b7bbef0350233" | ||||
|                 "sha256:69805d6b69f56eb05b62daea3a7dbd7aa44324ad1306445e05da8060232d00f4", | ||||
|                 "sha256:a8774e55b59fd9fc893b0d05e9bfc6f47081f46ff5b46f39ccf24631b7be356b" | ||||
|             ], | ||||
|             "markers": "python_version >= '3.6'", | ||||
|             "version": "==4.6" | ||||
|             "version": "==4.7" | ||||
|         }, | ||||
|         "ruamel.yaml": { | ||||
|             "hashes": [ | ||||
| @ -980,10 +971,10 @@ | ||||
|         }, | ||||
|         "s3transfer": { | ||||
|             "hashes": [ | ||||
|                 "sha256:2482b4259524933a022d59da830f51bd746db62f047d6eb213f2f8855dcb8a13", | ||||
|                 "sha256:921a37e2aefc64145e7b73d50c71bb4f26f46e4c9f414dc648c6245ff92cf7db" | ||||
|                 "sha256:1e28620e5b444652ed752cf87c7e0cb15b0e578972568c6609f0f18212f259ed", | ||||
|                 "sha256:7fdddb4f22275cf1d32129e21f056337fd2a80b6ccef1664528145b72c49e6d2" | ||||
|             ], | ||||
|             "version": "==0.3.3" | ||||
|             "version": "==0.3.4" | ||||
|         }, | ||||
|         "sentry-sdk": { | ||||
|             "hashes": [ | ||||
| @ -1017,11 +1008,11 @@ | ||||
|         }, | ||||
|         "structlog": { | ||||
|             "hashes": [ | ||||
|                 "sha256:7a48375db6274ed1d0ae6123c486472aa1d0890b08d314d2b016f3aa7f35990b", | ||||
|                 "sha256:8a672be150547a93d90a7d74229a29e765be05bd156a35cdcc527ebf68e9af92" | ||||
|                 "sha256:33dd6bd5f49355e52c1c61bb6a4f20d0b48ce0328cc4a45fe872d38b97a05ccd", | ||||
|                 "sha256:af79dfa547d104af8d60f86eac12fb54825f54a46bc998e4504ef66177103174" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==20.1.0" | ||||
|             "version": "==20.2.0" | ||||
|         }, | ||||
|         "swagger-spec-validator": { | ||||
|             "hashes": [ | ||||
| @ -1064,10 +1055,10 @@ | ||||
|         }, | ||||
|         "txaio": { | ||||
|             "hashes": [ | ||||
|                 "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d", | ||||
|                 "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae" | ||||
|                 "sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549", | ||||
|                 "sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f" | ||||
|             ], | ||||
|             "version": "==20.4.1" | ||||
|             "version": "==20.12.1" | ||||
|         }, | ||||
|         "uritemplate": { | ||||
|             "hashes": [ | ||||
| @ -1093,11 +1084,11 @@ | ||||
|                 "standard" | ||||
|             ], | ||||
|             "hashes": [ | ||||
|                 "sha256:2a7b17f4d9848d6557ccc2274a5f7c97f1daf037d130a0c6918f67cd9bc8cdf5", | ||||
|                 "sha256:6fcce74c00b77d4f4b3ed7ba1b2a370d27133bfdb46f835b7a76dfe0a8c110ae" | ||||
|                 "sha256:1079c50a06f6338095b4f203e7861dbff318dde5f22f3a324fc6e94c7654164c", | ||||
|                 "sha256:ef1e0bb5f7941c6fe324e06443ddac0331e1632a776175f87891c7bd02694355" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==0.13.1" | ||||
|             "version": "==0.13.3" | ||||
|         }, | ||||
|         "uvloop": { | ||||
|             "hashes": [ | ||||
| @ -1328,51 +1319,66 @@ | ||||
|         }, | ||||
|         "coverage": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516", | ||||
|                 "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259", | ||||
|                 "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9", | ||||
|                 "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097", | ||||
|                 "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0", | ||||
|                 "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f", | ||||
|                 "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7", | ||||
|                 "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c", | ||||
|                 "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5", | ||||
|                 "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7", | ||||
|                 "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729", | ||||
|                 "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978", | ||||
|                 "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9", | ||||
|                 "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f", | ||||
|                 "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9", | ||||
|                 "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822", | ||||
|                 "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418", | ||||
|                 "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82", | ||||
|                 "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f", | ||||
|                 "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d", | ||||
|                 "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221", | ||||
|                 "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4", | ||||
|                 "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21", | ||||
|                 "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709", | ||||
|                 "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54", | ||||
|                 "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d", | ||||
|                 "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270", | ||||
|                 "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24", | ||||
|                 "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751", | ||||
|                 "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a", | ||||
|                 "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237", | ||||
|                 "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7", | ||||
|                 "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636", | ||||
|                 "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8" | ||||
|                 "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297", | ||||
|                 "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1", | ||||
|                 "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497", | ||||
|                 "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606", | ||||
|                 "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528", | ||||
|                 "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b", | ||||
|                 "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4", | ||||
|                 "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830", | ||||
|                 "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1", | ||||
|                 "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f", | ||||
|                 "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d", | ||||
|                 "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3", | ||||
|                 "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8", | ||||
|                 "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500", | ||||
|                 "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7", | ||||
|                 "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb", | ||||
|                 "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b", | ||||
|                 "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059", | ||||
|                 "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b", | ||||
|                 "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72", | ||||
|                 "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36", | ||||
|                 "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277", | ||||
|                 "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c", | ||||
|                 "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631", | ||||
|                 "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff", | ||||
|                 "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8", | ||||
|                 "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec", | ||||
|                 "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b", | ||||
|                 "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7", | ||||
|                 "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105", | ||||
|                 "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b", | ||||
|                 "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c", | ||||
|                 "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b", | ||||
|                 "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98", | ||||
|                 "sha256:cd556c79ad665faeae28020a0ab3bda6cd47d94bec48e36970719b0b86e4dcf4", | ||||
|                 "sha256:ce6f3a147b4b1a8b09aae48517ae91139b1b010c5f36423fa2b866a8b23df879", | ||||
|                 "sha256:ceb499d2b3d1d7b7ba23abe8bf26df5f06ba8c71127f188333dddcf356b4b63f", | ||||
|                 "sha256:cef06fb382557f66d81d804230c11ab292d94b840b3cb7bf4450778377b592f4", | ||||
|                 "sha256:e448f56cfeae7b1b3b5bcd99bb377cde7c4eb1970a525c770720a352bc4c8044", | ||||
|                 "sha256:e52d3d95df81c8f6b2a1685aabffadf2d2d9ad97203a40f8d61e51b70f191e4e", | ||||
|                 "sha256:ee2f1d1c223c3d2c24e3afbb2dd38be3f03b1a8d6a83ee3d9eb8c36a52bee899", | ||||
|                 "sha256:f2c6888eada180814b8583c3e793f3f343a692fc802546eed45f40a001b1169f", | ||||
|                 "sha256:f51dbba78d68a44e99d484ca8c8f604f17e957c1ca09c3ebc2c7e3bbd9ba0448", | ||||
|                 "sha256:f54de00baf200b4539a5a092a759f000b5f45fd226d6d25a76b0dff71177a714", | ||||
|                 "sha256:fa10fee7e32213f5c7b0d6428ea92e3a3fdd6d725590238a3f92c0de1c78b9d2", | ||||
|                 "sha256:fabeeb121735d47d8eab8671b6b031ce08514c86b7ad8f7d5490a7b6dcd6267d", | ||||
|                 "sha256:fac3c432851038b3e6afe086f777732bcf7f6ebbfd90951fa04ee53db6d0bcdd", | ||||
|                 "sha256:fda29412a66099af6d6de0baa6bd7c52674de177ec2ad2630ca264142d69c6c7", | ||||
|                 "sha256:ff1330e8bc996570221b450e2d539134baa9465f5cb98aff0e0f73f34172e0ae" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==5.3" | ||||
|             "version": "==5.3.1" | ||||
|         }, | ||||
|         "django": { | ||||
|             "hashes": [ | ||||
|                 "sha256:5c866205f15e7a7123f1eec6ab939d22d5bde1416635cab259684af66d8e48a2", | ||||
|                 "sha256:edb10b5c45e7e9c0fb1dc00b76ec7449aca258a39ffd613dbd078c51d19c9f03" | ||||
|                 "sha256:2d78425ba74c7a1a74b196058b261b9733a8570782f4e2828974777ccca7edf7", | ||||
|                 "sha256:efa2ab96b33b20c2182db93147a0c3cd7769d418926f9e9f140a60dca7c64ca9" | ||||
|             ], | ||||
|             "index": "pypi", | ||||
|             "version": "==3.1.4" | ||||
|             "version": "==3.1.5" | ||||
|         }, | ||||
|         "django-debug-toolbar": { | ||||
|             "hashes": [ | ||||
| @ -1412,10 +1418,10 @@ | ||||
|         }, | ||||
|         "gitpython": { | ||||
|             "hashes": [ | ||||
|                 "sha256:6eea89b655917b500437e9668e4a12eabdcf00229a0df1762aabd692ef9b746b", | ||||
|                 "sha256:befa4d101f91bad1b632df4308ec64555db684c360bd7d2130b4807d49ce86b8" | ||||
|                 "sha256:42dbefd8d9e2576c496ed0059f3103dcef7125b9ce16f9d5f9c834aed44a1dac", | ||||
|                 "sha256:867ec3dfb126aac0f8296b19fb63b8c4a399f32b4b6fafe84c4b10af5fa9f7b5" | ||||
|             ], | ||||
|             "version": "==3.1.11" | ||||
|             "version": "==3.1.12" | ||||
|         }, | ||||
|         "iniconfig": { | ||||
|             "hashes": [ | ||||
| @ -1602,10 +1608,10 @@ | ||||
|         }, | ||||
|         "pytz": { | ||||
|             "hashes": [ | ||||
|                 "sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268", | ||||
|                 "sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd" | ||||
|                 "sha256:16962c5fb8db4a8f63a26646d8886e9d769b6c511543557bc84e9569fb9a9cb4", | ||||
|                 "sha256:180befebb1927b16f6b57101720075a984c019ac16b1b7575673bea42c6c3da5" | ||||
|             ], | ||||
|             "version": "==2020.4" | ||||
|             "version": "==2020.5" | ||||
|         }, | ||||
|         "pyyaml": { | ||||
|             "hashes": [ | ||||
| @ -1736,38 +1742,38 @@ | ||||
|         }, | ||||
|         "typed-ast": { | ||||
|             "hashes": [ | ||||
|                 "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355", | ||||
|                 "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919", | ||||
|                 "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d", | ||||
|                 "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa", | ||||
|                 "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652", | ||||
|                 "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75", | ||||
|                 "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c", | ||||
|                 "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01", | ||||
|                 "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d", | ||||
|                 "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1", | ||||
|                 "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907", | ||||
|                 "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c", | ||||
|                 "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3", | ||||
|                 "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d", | ||||
|                 "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b", | ||||
|                 "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614", | ||||
|                 "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c", | ||||
|                 "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb", | ||||
|                 "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395", | ||||
|                 "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b", | ||||
|                 "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41", | ||||
|                 "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6", | ||||
|                 "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34", | ||||
|                 "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe", | ||||
|                 "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072", | ||||
|                 "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298", | ||||
|                 "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91", | ||||
|                 "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4", | ||||
|                 "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f", | ||||
|                 "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7" | ||||
|                 "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1", | ||||
|                 "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d", | ||||
|                 "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6", | ||||
|                 "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd", | ||||
|                 "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37", | ||||
|                 "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151", | ||||
|                 "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07", | ||||
|                 "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440", | ||||
|                 "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70", | ||||
|                 "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496", | ||||
|                 "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea", | ||||
|                 "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400", | ||||
|                 "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc", | ||||
|                 "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606", | ||||
|                 "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc", | ||||
|                 "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581", | ||||
|                 "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412", | ||||
|                 "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a", | ||||
|                 "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2", | ||||
|                 "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787", | ||||
|                 "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f", | ||||
|                 "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937", | ||||
|                 "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64", | ||||
|                 "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487", | ||||
|                 "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b", | ||||
|                 "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41", | ||||
|                 "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a", | ||||
|                 "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3", | ||||
|                 "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166", | ||||
|                 "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10" | ||||
|             ], | ||||
|             "version": "==1.4.1" | ||||
|             "version": "==1.4.2" | ||||
|         }, | ||||
|         "typing-extensions": { | ||||
|             "hashes": [ | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| <img src="web/icons/icon_top_brand.svg" height="250" alt="authentik logo"> | ||||
| <img src="https://goauthentik.io/img/icon_top_brand_colour.svg" height="250" alt="authentik logo"> | ||||
|  | ||||
| --- | ||||
|  | ||||
| @ -21,8 +21,8 @@ For bigger setups, there is a Helm Chart in the `helm/` directory. This is docum | ||||
|  | ||||
| ## Screenshots | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Development | ||||
|  | ||||
|  | ||||
							
								
								
									
										12
									
								
								SECURITY.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								SECURITY.md
									
									
									
									
									
								
							| @ -2,13 +2,11 @@ | ||||
|  | ||||
| ## Supported Versions | ||||
|  | ||||
| As authentik is currently in a pre-stable, only the latest "stable" version is supported. After authentik 1.0, this will change. | ||||
|  | ||||
| | Version  | Supported          | | ||||
| | -------- | ------------------ | | ||||
| | 0.11.x   | :white_check_mark: | | ||||
| | 0.12.x   | :white_check_mark: | | ||||
| | 0.13.x   | :white_check_mark: | | ||||
| | Version    | Supported          | | ||||
| | ---------- | ------------------ | | ||||
| | 0.13.x     | :white_check_mark: | | ||||
| | 0.14.x     | :white_check_mark: | | ||||
| | 2021.1.x   | :white_check_mark: | | ||||
|  | ||||
| ## Reporting a Vulnerability | ||||
|  | ||||
|  | ||||
| @ -1,2 +1,2 @@ | ||||
| """authentik""" | ||||
| __version__ = "0.13.4-stable" | ||||
| __version__ = "2021.1.1-rc1" | ||||
|  | ||||
| @ -4,10 +4,9 @@ from collections import Counter | ||||
| from datetime import timedelta | ||||
| from typing import Dict, List | ||||
|  | ||||
| from django.db.models import Count, ExpressionWrapper, F | ||||
| 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.http import response | ||||
| from django.utils.timezone import now | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| from rest_framework.fields import SerializerMethodField | ||||
| @ -17,7 +16,7 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import Serializer | ||||
| from rest_framework.viewsets import ViewSet | ||||
|  | ||||
| from authentik.audit.models import Event, EventAction | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]: | ||||
| @ -60,10 +59,10 @@ class AdministrationMetricsSerializer(Serializer): | ||||
|         """Get failed logins per hour for the last 24 hours""" | ||||
|         return get_events_per_1h(action=EventAction.LOGIN_FAILED) | ||||
|  | ||||
|     def create(self, request: Request) -> response: | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.contrib import messages | ||||
| from django.db.models import Model | ||||
| from django.http.response import Http404 | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| @ -26,10 +27,10 @@ class TaskSerializer(Serializer): | ||||
|     status = IntegerField(source="result.status.value") | ||||
|     messages = ListField(source="result.messages") | ||||
|  | ||||
|     def create(self, request: Request) -> Response: | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| """authentik administration overview""" | ||||
| from django.core.cache import cache | ||||
| from django.db.models import Model | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| from packaging.version import parse | ||||
| from rest_framework.fields import SerializerMethodField | ||||
| @ -39,10 +40,10 @@ class VersionSerializer(Serializer): | ||||
|             self.get_version_latest(instance) | ||||
|         ) | ||||
|  | ||||
|     def create(self, request: Request) -> Response: | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| @ -51,7 +52,7 @@ class VersionViewSet(ListModelMixin, GenericViewSet): | ||||
|  | ||||
|     permission_classes = [IsAdminUser] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|     def get_queryset(self):  # pragma: no cover | ||||
|         return None | ||||
|  | ||||
|     @swagger_auto_schema(responses={200: VersionSerializer(many=True)}) | ||||
|  | ||||
| @ -15,7 +15,7 @@ class WorkerViewSet(ListModelMixin, GenericViewSet): | ||||
|     serializer_class = Serializer | ||||
|     permission_classes = [IsAdminUser] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|     def get_queryset(self):  # pragma: no cover | ||||
|         return None | ||||
|  | ||||
|     def list(self, request: Request) -> Response: | ||||
|  | ||||
| @ -14,4 +14,6 @@ SOURCE_SERIALIZER_FIELDS = [ | ||||
|     "enabled", | ||||
|     "authentication_flow", | ||||
|     "enrollment_flow", | ||||
|     "verbose_name", | ||||
|     "verbose_name_plural", | ||||
| ] | ||||
|  | ||||
| @ -1,8 +1,11 @@ | ||||
| """authentik admin tasks""" | ||||
| from django.core.cache import cache | ||||
| from packaging.version import parse | ||||
| from requests import RequestException, get | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import __version__ | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| @ -19,12 +22,24 @@ def update_latest_version(self: MonitoredTask): | ||||
|         response.raise_for_status() | ||||
|         data = response.json() | ||||
|         tag_name = data.get("tag_name") | ||||
|         cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT) | ||||
|         upstream_version = tag_name.split("/")[1] | ||||
|         cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status( | ||||
|             TaskResult( | ||||
|                 TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"] | ||||
|             ) | ||||
|         ) | ||||
|         # Check if upstream version is newer than what we're running, | ||||
|         # and if no event exists yet, create one. | ||||
|         local_version = parse(__version__) | ||||
|         if local_version < parse(upstream_version): | ||||
|             # Event has already been created, don't create duplicate | ||||
|             if Event.objects.filter( | ||||
|                 action=EventAction.UPDATE_AVAILABLE, | ||||
|                 context__new_version=upstream_version, | ||||
|             ).exists(): | ||||
|                 return | ||||
|             Event.new(EventAction.UPDATE_AVAILABLE, new_version=upstream_version).save() | ||||
|     except (RequestException, IndexError) as exc: | ||||
|         cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT) | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|  | ||||
| @ -1,131 +0,0 @@ | ||||
| {% extends "administration/base.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load authentik_utils %} | ||||
|  | ||||
| {% block content %} | ||||
| <section class="pf-c-page__main-section pf-m-light"> | ||||
|     <div class="pf-c-content"> | ||||
|         <h1> | ||||
|             <i class="pf-icon pf-icon-applications"></i> | ||||
|             {% trans 'Applications' %} | ||||
|         </h1> | ||||
|         <p>{% trans "External Applications which use authentik as Identity-Provider, utilizing protocols like OAuth2 and SAML." %}</p> | ||||
|     </div> | ||||
| </section> | ||||
| <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|     <div class="pf-c-card"> | ||||
|         {% if object_list %} | ||||
|         <div class="pf-c-toolbar"> | ||||
|             <div class="pf-c-toolbar__content"> | ||||
|                 {% include 'partials/toolbar_search.html' %} | ||||
|                 <div class="pf-c-toolbar__bulk-select"> | ||||
|                     <ak-modal-button href="{% url 'authentik_admin:application-create' %}"> | ||||
|                         <ak-spinner-button slot="trigger" class="pf-m-primary"> | ||||
|                             {% trans 'Create' %} | ||||
|                         </ak-spinner-button> | ||||
|                         <div slot="modal"></div> | ||||
|                     </ak-modal-button> | ||||
|                     <button role="ak-refresh" class="pf-c-button pf-m-primary"> | ||||
|                         {% trans 'Refresh' %} | ||||
|                     </button> | ||||
|                 </div> | ||||
|                 {% include 'partials/pagination.html' %} | ||||
|             </div> | ||||
|         </div> | ||||
|         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||
|             <thead> | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader"></th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Name' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Slug' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Provider' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Provider Type' %}</th> | ||||
|                     <th role="columnheader"></th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
|             <tbody role="rowgroup"> | ||||
|                 {% for application in object_list %} | ||||
|                 <tr role="row"> | ||||
|                     <td role="cell" {% if application.meta_icon %} style="vertical-align: bottom;" {% endif %}> | ||||
|                         {% if application.meta_icon %} | ||||
|                         <img class="app-icon pf-c-avatar" src="{{ application.meta_icon.url }}" alt="{% trans 'Application Icon' %}"> | ||||
|                         {% else %} | ||||
|                         <i class="pf-icon pf-icon-arrow"></i> | ||||
|                         {% endif %} | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <a href="/applications/{{ application.slug }}/"> | ||||
|                             <div> | ||||
|                                 {{ application.name }} | ||||
|                             </div> | ||||
|                             {% if application.meta_publisher %} | ||||
|                             <small>{{ application.meta_publisher }}</small> | ||||
|                             {% endif %} | ||||
|                         </a> | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <code>{{ application.slug }}</span> | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ application.get_provider }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ application.get_provider|verbose_name }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td> | ||||
|                         <ak-modal-button href="{% url 'authentik_admin:application-update' pk=application.pk %}"> | ||||
|                             <ak-spinner-button slot="trigger" class="pf-m-secondary"> | ||||
|                                 {% trans 'Edit' %} | ||||
|                             </ak-spinner-button> | ||||
|                             <div slot="modal"></div> | ||||
|                         </ak-modal-button> | ||||
|                         <ak-modal-button href="{% url 'authentik_admin:application-delete' pk=application.pk %}"> | ||||
|                             <ak-spinner-button slot="trigger" class="pf-m-danger"> | ||||
|                                 {% trans 'Delete' %} | ||||
|                             </ak-spinner-button> | ||||
|                             <div slot="modal"></div> | ||||
|                         </ak-modal-button> | ||||
|                     </td> | ||||
|                 </tr> | ||||
|                 {% endfor %} | ||||
|             </tbody> | ||||
|         </table> | ||||
|         <div class="pf-c-pagination pf-m-bottom"> | ||||
|             {% include 'partials/pagination.html' %} | ||||
|         </div> | ||||
|         {% else %} | ||||
|         <div class="pf-c-toolbar"> | ||||
|             <div class="pf-c-toolbar__content"> | ||||
|                 {% include 'partials/toolbar_search.html' %} | ||||
|             </div> | ||||
|         </div> | ||||
|         <div class="pf-c-empty-state"> | ||||
|             <div class="pf-c-empty-state__content"> | ||||
|                 <i class="pf-icon pf-icon-applications pf-c-empty-state__icon" aria-hidden="true"></i> | ||||
|                 <h1 class="pf-c-title pf-m-lg"> | ||||
|                     {% trans 'No Applications.' %} | ||||
|                 </h1> | ||||
|                 <div class="pf-c-empty-state__body"> | ||||
|                 {% if request.GET.search != "" %} | ||||
|                     {% trans "Your search query doesn't match any application." %} | ||||
|                 {% else %} | ||||
|                     {% trans 'Currently no applications exist. Click the button below to create one.' %} | ||||
|                 {% endif %} | ||||
|                 </div> | ||||
|                 <ak-modal-button href="{% url 'authentik_admin:application-create' %}"> | ||||
|                     <ak-spinner-button slot="trigger" class="pf-m-primary"> | ||||
|                         {% trans 'Create' %} | ||||
|                     </ak-spinner-button> | ||||
|                     <div slot="modal"></div> | ||||
|                 </ak-modal-button> | ||||
|             </div> | ||||
|         </div> | ||||
|         {% endif %} | ||||
|     </div> | ||||
| </section> | ||||
| {% endblock %} | ||||
| @ -53,7 +53,7 @@ | ||||
|                 {% for flow in object_list %} | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader"> | ||||
|                         <a href="/flows/{{ flow.slug }}/"> | ||||
|                         <a href="/flows/{{ flow.slug }}"> | ||||
|                             <div><code>{{ flow.slug }}</code></div> | ||||
|                             <small>{{ flow.name }}</small> | ||||
|                         </a> | ||||
|  | ||||
| @ -41,6 +41,17 @@ | ||||
|                                 </ak-modal-button> | ||||
|                             </li> | ||||
|                             {% endfor %} | ||||
|                             <li> | ||||
|                                 <ak-modal-button href="{% url 'authentik_admin:provider-saml-from-metadata' %}"> | ||||
|                                     <button slot="trigger" class="pf-c-dropdown__menu-item"> | ||||
|                                         {% trans 'SAML Provider from Metadata' %}<br> | ||||
|                                         <small> | ||||
|                                             {% trans "Create a SAML Provider by importing its Metadata." %} | ||||
|                                         </small> | ||||
|                                     </button> | ||||
|                                     <div slot="modal"></div> | ||||
|                                 </ak-modal-button> | ||||
|                             </li> | ||||
|                         </ul> | ||||
|                     </ak-dropdown> | ||||
|                     <button role="ak-refresh" class="pf-c-button pf-m-primary"> | ||||
|  | ||||
| @ -37,8 +37,9 @@ | ||||
|         <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||
|             <thead> | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader" scope="col">{% trans 'ID' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Created by' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Expiry' %}</th> | ||||
|                     <th role="columnheader" scope="col">{% trans 'Link' %}</th> | ||||
|                     <th role="cell"></th> | ||||
|                 </tr> | ||||
|             </thead> | ||||
| @ -47,12 +48,17 @@ | ||||
|                 <tr role="row"> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ invitation.expiry }} | ||||
|                             {{ invitation.invite_uuid }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ invitation.Link }} | ||||
|                             {{ invitation.created_by }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|                             {{ invitation.expiry|default:"-" }} | ||||
|                         </span> | ||||
|                     </td> | ||||
|                     <td> | ||||
|  | ||||
| @ -38,7 +38,7 @@ | ||||
|                 {% for task in object_list %} | ||||
|                 <tr role="row"> | ||||
|                     <th role="columnheader"> | ||||
|                         <pre>{{ task.task_name }}</pre> | ||||
|                         <span>{{ task.html_name|join:"_­" }}</span> | ||||
|                     </th> | ||||
|                     <td role="cell"> | ||||
|                         <span> | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| from django import template | ||||
| from django.db.models import Model | ||||
| from django.utils.html import mark_safe | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| register = template.Library() | ||||
| LOGGER = get_logger() | ||||
|  | ||||
							
								
								
									
										76
									
								
								authentik/admin/tests/test_tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								authentik/admin/tests/test_tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| """test admin tasks""" | ||||
| import json | ||||
| from dataclasses import dataclass | ||||
| from unittest.mock import Mock, patch | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.test import TestCase | ||||
| from requests.exceptions import RequestException | ||||
|  | ||||
| from authentik.admin.tasks import VERSION_CACHE_KEY, update_latest_version | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class MockResponse: | ||||
|     """Mock class to emulate the methods of requests's Response we need""" | ||||
|  | ||||
|     status_code: int | ||||
|     response: str | ||||
|  | ||||
|     def json(self) -> dict: | ||||
|         """Get json parsed response""" | ||||
|         return json.loads(self.response) | ||||
|  | ||||
|     def raise_for_status(self): | ||||
|         """raise RequestException if status code is 400 or more""" | ||||
|         if self.status_code >= 400: | ||||
|             raise RequestException | ||||
|  | ||||
|  | ||||
| REQUEST_MOCK_VALID = Mock( | ||||
|     return_value=MockResponse( | ||||
|         200, | ||||
|         """{ | ||||
|             "tag_name": "version/99999999.9999999" | ||||
|         }""", | ||||
|     ) | ||||
| ) | ||||
|  | ||||
| REQUEST_MOCK_INVALID = Mock(return_value=MockResponse(400, "{}")) | ||||
|  | ||||
|  | ||||
| class TestAdminTasks(TestCase): | ||||
|     """test admin tasks""" | ||||
|  | ||||
|     @patch("authentik.admin.tasks.get", REQUEST_MOCK_VALID) | ||||
|     def test_version_valid_response(self): | ||||
|         """Test Update checker with valid response""" | ||||
|         update_latest_version.delay().get() | ||||
|         self.assertEqual(cache.get(VERSION_CACHE_KEY), "99999999.9999999") | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.UPDATE_AVAILABLE, context__new_version="99999999.9999999" | ||||
|             ).exists() | ||||
|         ) | ||||
|         # test that a consecutive check doesn't create a duplicate event | ||||
|         update_latest_version.delay().get() | ||||
|         self.assertEqual( | ||||
|             len( | ||||
|                 Event.objects.filter( | ||||
|                     action=EventAction.UPDATE_AVAILABLE, context__new_version="99999999.9999999" | ||||
|                 ) | ||||
|             ), | ||||
|             1, | ||||
|         ) | ||||
|  | ||||
|     @patch("authentik.admin.tasks.get", REQUEST_MOCK_INVALID) | ||||
|     def test_version_error(self): | ||||
|         """Test Update checker with invalid response""" | ||||
|         update_latest_version.delay().get() | ||||
|         self.assertEqual(cache.get(VERSION_CACHE_KEY), "0.0.0") | ||||
|         self.assertFalse( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.UPDATE_AVAILABLE, context__new_version="0.0.0" | ||||
|             ).exists() | ||||
|         ) | ||||
| @ -4,6 +4,8 @@ from django.urls import path | ||||
| from authentik.admin.views import ( | ||||
|     applications, | ||||
|     certificate_key_pair, | ||||
|     events_notifications_rules, | ||||
|     events_notifications_transports, | ||||
|     flows, | ||||
|     groups, | ||||
|     outposts, | ||||
| @ -22,6 +24,7 @@ from authentik.admin.views import ( | ||||
|     tokens, | ||||
|     users, | ||||
| ) | ||||
| from authentik.providers.saml.views import MetadataImportView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     path( | ||||
| @ -35,9 +38,6 @@ urlpatterns = [ | ||||
|         name="overview-clear-policy-cache", | ||||
|     ), | ||||
|     # Applications | ||||
|     path( | ||||
|         "applications/", applications.ApplicationListView.as_view(), name="applications" | ||||
|     ), | ||||
|     path( | ||||
|         "applications/create/", | ||||
|         applications.ApplicationCreateView.as_view(), | ||||
| @ -119,6 +119,11 @@ urlpatterns = [ | ||||
|         providers.ProviderCreateView.as_view(), | ||||
|         name="provider-create", | ||||
|     ), | ||||
|     path( | ||||
|         "providers/create/saml/from-metadata/", | ||||
|         MetadataImportView.as_view(), | ||||
|         name="provider-saml-from-metadata", | ||||
|     ), | ||||
|     path( | ||||
|         "providers/<int:pk>/update/", | ||||
|         providers.ProviderUpdateView.as_view(), | ||||
| @ -349,4 +354,36 @@ urlpatterns = [ | ||||
|         tasks.TaskListView.as_view(), | ||||
|         name="tasks", | ||||
|     ), | ||||
|     # Event Notification Transpots | ||||
|     path( | ||||
|         "events/transports/create/", | ||||
|         events_notifications_transports.NotificationTransportCreateView.as_view(), | ||||
|         name="notification-transport-create", | ||||
|     ), | ||||
|     path( | ||||
|         "events/transports/<uuid:pk>/update/", | ||||
|         events_notifications_transports.NotificationTransportUpdateView.as_view(), | ||||
|         name="notification-transport-update", | ||||
|     ), | ||||
|     path( | ||||
|         "events/transports/<uuid:pk>/delete/", | ||||
|         events_notifications_transports.NotificationTransportDeleteView.as_view(), | ||||
|         name="notification-transport-delete", | ||||
|     ), | ||||
|     # Event Notification Rules | ||||
|     path( | ||||
|         "events/rules/create/", | ||||
|         events_notifications_rules.NotificationRuleCreateView.as_view(), | ||||
|         name="notification-rule-create", | ||||
|     ), | ||||
|     path( | ||||
|         "events/rules/<uuid:pk>/update/", | ||||
|         events_notifications_rules.NotificationRuleUpdateView.as_view(), | ||||
|         name="notification-rule-update", | ||||
|     ), | ||||
|     path( | ||||
|         "events/rules/<uuid:pk>/delete/", | ||||
|         events_notifications_rules.NotificationRuleDeleteView.as_view(), | ||||
|         name="notification-rule-delete", | ||||
|     ), | ||||
| ] | ||||
|  | ||||
| @ -6,44 +6,15 @@ from django.contrib.auth.mixins import ( | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import ListView, UpdateView | ||||
| from guardian.mixins import PermissionListMixin, PermissionRequiredMixin | ||||
| from django.views.generic import UpdateView | ||||
| from guardian.mixins import PermissionRequiredMixin | ||||
|  | ||||
| from authentik.admin.views.utils import ( | ||||
|     BackSuccessUrlMixin, | ||||
|     DeleteMessageView, | ||||
|     SearchListMixin, | ||||
|     UserPaginateListMixin, | ||||
| ) | ||||
| from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView | ||||
| from authentik.core.forms.applications import ApplicationForm | ||||
| from authentik.core.models import Application | ||||
| from authentik.lib.views import CreateAssignPermView | ||||
|  | ||||
|  | ||||
| class ApplicationListView( | ||||
|     LoginRequiredMixin, | ||||
|     PermissionListMixin, | ||||
|     UserPaginateListMixin, | ||||
|     SearchListMixin, | ||||
|     ListView, | ||||
| ): | ||||
|     """Show list of all applications""" | ||||
|  | ||||
|     model = Application | ||||
|     permission_required = "authentik_core.view_application" | ||||
|     ordering = "name" | ||||
|     template_name = "administration/application/list.html" | ||||
|  | ||||
|     search_fields = [ | ||||
|         "name", | ||||
|         "slug", | ||||
|         "meta_launch_url", | ||||
|         "meta_icon_url", | ||||
|         "meta_description", | ||||
|         "meta_publisher", | ||||
|     ] | ||||
|  | ||||
|  | ||||
| class ApplicationCreateView( | ||||
|     SuccessMessageMixin, | ||||
|     BackSuccessUrlMixin, | ||||
| @ -58,7 +29,7 @@ class ApplicationCreateView( | ||||
|     permission_required = "authentik_core.add_application" | ||||
|  | ||||
|     template_name = "generic/create.html" | ||||
|     success_url = reverse_lazy("authentik_admin:applications") | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully created Application") | ||||
|  | ||||
|  | ||||
| @ -76,7 +47,7 @@ class ApplicationUpdateView( | ||||
|     permission_required = "authentik_core.change_application" | ||||
|  | ||||
|     template_name = "generic/update.html" | ||||
|     success_url = reverse_lazy("authentik_admin:applications") | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully updated Application") | ||||
|  | ||||
|  | ||||
| @ -89,5 +60,5 @@ class ApplicationDeleteView( | ||||
|     permission_required = "authentik_core.delete_application" | ||||
|  | ||||
|     template_name = "generic/delete.html" | ||||
|     success_url = reverse_lazy("authentik_admin:applications") | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully deleted Application") | ||||
|  | ||||
							
								
								
									
										64
									
								
								authentik/admin/views/events_notifications_rules.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								authentik/admin/views/events_notifications_rules.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| """authentik NotificationRule administration""" | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.mixins import ( | ||||
|     PermissionRequiredMixin as DjangoPermissionRequiredMixin, | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import UpdateView | ||||
| from guardian.mixins import PermissionRequiredMixin | ||||
|  | ||||
| from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView | ||||
| from authentik.events.forms import NotificationRuleForm | ||||
| from authentik.events.models import NotificationRule | ||||
| from authentik.lib.views import CreateAssignPermView | ||||
|  | ||||
|  | ||||
| class NotificationRuleCreateView( | ||||
|     SuccessMessageMixin, | ||||
|     BackSuccessUrlMixin, | ||||
|     LoginRequiredMixin, | ||||
|     DjangoPermissionRequiredMixin, | ||||
|     CreateAssignPermView, | ||||
| ): | ||||
|     """Create new NotificationRule""" | ||||
|  | ||||
|     model = NotificationRule | ||||
|     form_class = NotificationRuleForm | ||||
|     permission_required = "authentik_events.add_NotificationRule" | ||||
|  | ||||
|     template_name = "generic/create.html" | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully created Notification Rule") | ||||
|  | ||||
|  | ||||
| class NotificationRuleUpdateView( | ||||
|     SuccessMessageMixin, | ||||
|     BackSuccessUrlMixin, | ||||
|     LoginRequiredMixin, | ||||
|     PermissionRequiredMixin, | ||||
|     UpdateView, | ||||
| ): | ||||
|     """Update application""" | ||||
|  | ||||
|     model = NotificationRule | ||||
|     form_class = NotificationRuleForm | ||||
|     permission_required = "authentik_events.change_NotificationRule" | ||||
|  | ||||
|     template_name = "generic/update.html" | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully updated Notification Rule") | ||||
|  | ||||
|  | ||||
| class NotificationRuleDeleteView( | ||||
|     LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView | ||||
| ): | ||||
|     """Delete application""" | ||||
|  | ||||
|     model = NotificationRule | ||||
|     permission_required = "authentik_events.delete_NotificationRule" | ||||
|  | ||||
|     template_name = "generic/delete.html" | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully deleted Notification Rule") | ||||
							
								
								
									
										64
									
								
								authentik/admin/views/events_notifications_transports.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								authentik/admin/views/events_notifications_transports.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,64 @@ | ||||
| """authentik NotificationTransport administration""" | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.contrib.auth.mixins import ( | ||||
|     PermissionRequiredMixin as DjangoPermissionRequiredMixin, | ||||
| ) | ||||
| from django.contrib.messages.views import SuccessMessageMixin | ||||
| from django.urls import reverse_lazy | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import UpdateView | ||||
| from guardian.mixins import PermissionRequiredMixin | ||||
|  | ||||
| from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView | ||||
| from authentik.events.forms import NotificationTransportForm | ||||
| from authentik.events.models import NotificationTransport | ||||
| from authentik.lib.views import CreateAssignPermView | ||||
|  | ||||
|  | ||||
| class NotificationTransportCreateView( | ||||
|     SuccessMessageMixin, | ||||
|     BackSuccessUrlMixin, | ||||
|     LoginRequiredMixin, | ||||
|     DjangoPermissionRequiredMixin, | ||||
|     CreateAssignPermView, | ||||
| ): | ||||
|     """Create new NotificationTransport""" | ||||
|  | ||||
|     model = NotificationTransport | ||||
|     form_class = NotificationTransportForm | ||||
|     permission_required = "authentik_events.add_notificationtransport" | ||||
|  | ||||
|     template_name = "generic/create.html" | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully created Notification Transport") | ||||
|  | ||||
|  | ||||
| class NotificationTransportUpdateView( | ||||
|     SuccessMessageMixin, | ||||
|     BackSuccessUrlMixin, | ||||
|     LoginRequiredMixin, | ||||
|     PermissionRequiredMixin, | ||||
|     UpdateView, | ||||
| ): | ||||
|     """Update application""" | ||||
|  | ||||
|     model = NotificationTransport | ||||
|     form_class = NotificationTransportForm | ||||
|     permission_required = "authentik_events.change_notificationtransport" | ||||
|  | ||||
|     template_name = "generic/update.html" | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully updated Notification Transport") | ||||
|  | ||||
|  | ||||
| class NotificationTransportDeleteView( | ||||
|     LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView | ||||
| ): | ||||
|     """Delete application""" | ||||
|  | ||||
|     model = NotificationTransport | ||||
|     permission_required = "authentik_events.delete_notificationtransport" | ||||
|  | ||||
|     template_name = "generic/delete.html" | ||||
|     success_url = reverse_lazy("authentik_core:shell") | ||||
|     success_message = _("Successfully deleted Notification Transport") | ||||
| @ -5,10 +5,11 @@ from django.http.request import HttpRequest | ||||
| from django.http.response import HttpResponse | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.generic import FormView | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.admin.forms.overview import FlowCacheClearForm, PolicyCacheClearForm | ||||
| from authentik.admin.mixins import AdminRequiredMixin | ||||
| from authentik.core.api.applications import user_app_cache_key | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -26,6 +27,9 @@ class PolicyCacheClearView(AdminRequiredMixin, SuccessMessageMixin, FormView): | ||||
|         keys = cache.keys("policy_*") | ||||
|         cache.delete_many(keys) | ||||
|         LOGGER.debug("Cleared Policy cache", keys=len(keys)) | ||||
|         # Also delete user application cache | ||||
|         keys = user_app_cache_key("*") | ||||
|         cache.delete_many(keys) | ||||
|         return super().post(request, *args, **kwargs) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -19,7 +19,6 @@ from authentik.admin.views.utils import ( | ||||
| from authentik.lib.views import CreateAssignPermView | ||||
| from authentik.stages.invitation.forms import InvitationForm | ||||
| from authentik.stages.invitation.models import Invitation | ||||
| from authentik.stages.invitation.signals import invitation_created | ||||
|  | ||||
|  | ||||
| class InvitationListView( | ||||
| @ -59,7 +58,6 @@ class InvitationCreateView( | ||||
|         obj = form.save(commit=False) | ||||
|         obj.created_by = self.request.user | ||||
|         obj.save() | ||||
|         invitation_created.send(sender=self, request=self.request, invitation=obj) | ||||
|         return HttpResponseRedirect(self.success_url) | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -5,7 +5,7 @@ from typing import Any, Optional, Tuple, Union | ||||
|  | ||||
| from rest_framework.authentication import BaseAuthentication, get_authorization_header | ||||
| from rest_framework.request import Request | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import Token, TokenIntents, User | ||||
|  | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """core Configs API""" | ||||
| from django.db.models import Model | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| from rest_framework.permissions import AllowAny | ||||
| from rest_framework.request import Request | ||||
| @ -19,10 +20,10 @@ class ConfigSerializer(Serializer): | ||||
|     error_reporting_environment = ReadOnlyField() | ||||
|     error_reporting_send_pii = ReadOnlyField() | ||||
|  | ||||
|     def create(self, request: Request) -> Response: | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| """core messages API""" | ||||
| from django.contrib.messages import get_messages | ||||
| from django.db.models import Model | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| from rest_framework.permissions import AllowAny | ||||
| from rest_framework.request import Request | ||||
| @ -17,10 +18,10 @@ class MessageSerializer(Serializer): | ||||
|     extra_tags = ReadOnlyField() | ||||
|     level_tag = ReadOnlyField() | ||||
|  | ||||
|     def create(self, request: Request) -> Response: | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, request: Request) -> Response: | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
|  | ||||
| @ -11,7 +11,6 @@ from authentik.admin.api.version import VersionViewSet | ||||
| from authentik.admin.api.workers import WorkerViewSet | ||||
| from authentik.api.v2.config import ConfigsViewSet | ||||
| from authentik.api.v2.messages import MessagesViewSet | ||||
| from authentik.audit.api import EventViewSet | ||||
| from authentik.core.api.applications import ApplicationViewSet | ||||
| from authentik.core.api.groups import GroupViewSet | ||||
| from authentik.core.api.propertymappings import PropertyMappingViewSet | ||||
| @ -20,6 +19,10 @@ from authentik.core.api.sources import SourceViewSet | ||||
| from authentik.core.api.tokens import TokenViewSet | ||||
| from authentik.core.api.users import UserViewSet | ||||
| from authentik.crypto.api import CertificateKeyPairViewSet | ||||
| from authentik.events.api.event import EventViewSet | ||||
| from authentik.events.api.notification import NotificationViewSet | ||||
| from authentik.events.api.notification_rule import NotificationRuleViewSet | ||||
| from authentik.events.api.notification_transport import NotificationTransportViewSet | ||||
| from authentik.flows.api import ( | ||||
|     FlowCacheViewSet, | ||||
|     FlowStageBindingViewSet, | ||||
| @ -37,6 +40,7 @@ from authentik.policies.api import ( | ||||
|     PolicyViewSet, | ||||
| ) | ||||
| from authentik.policies.dummy.api import DummyPolicyViewSet | ||||
| from authentik.policies.event_matcher.api import EventMatcherPolicyViewSet | ||||
| from authentik.policies.expiry.api import PasswordExpiryPolicyViewSet | ||||
| from authentik.policies.expression.api import ExpressionPolicyViewSet | ||||
| from authentik.policies.group_membership.api import GroupMembershipPolicyViewSet | ||||
| @ -96,7 +100,10 @@ router.register("flows/bindings", FlowStageBindingViewSet) | ||||
|  | ||||
| router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet) | ||||
|  | ||||
| router.register("audit/events", EventViewSet) | ||||
| router.register("events/events", EventViewSet) | ||||
| router.register("events/notifications", NotificationViewSet) | ||||
| router.register("events/transports", NotificationTransportViewSet) | ||||
| router.register("events/rules", NotificationRuleViewSet) | ||||
|  | ||||
| router.register("sources/all", SourceViewSet) | ||||
| router.register("sources/ldap", LDAPSourceViewSet) | ||||
| @ -107,6 +114,7 @@ router.register("policies/all", PolicyViewSet) | ||||
| router.register("policies/cached", PolicyCacheViewSet, basename="policies_cache") | ||||
| router.register("policies/bindings", PolicyBindingViewSet) | ||||
| router.register("policies/expression", ExpressionPolicyViewSet) | ||||
| router.register("policies/event_matcher", EventMatcherPolicyViewSet) | ||||
| router.register("policies/group_membership", GroupMembershipPolicyViewSet) | ||||
| router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) | ||||
| router.register("policies/password_expiry", PasswordExpiryPolicyViewSet) | ||||
| @ -148,7 +156,9 @@ info = openapi.Info( | ||||
|     title="authentik API", | ||||
|     default_version="v2", | ||||
|     contact=openapi.Contact(email="hello@beryju.org"), | ||||
|     license=openapi.License(name="MIT License"), | ||||
|     license=openapi.License( | ||||
|         name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE" | ||||
|     ), | ||||
| ) | ||||
| SchemaView = get_schema_view( | ||||
|     info, | ||||
|  | ||||
| @ -1,16 +0,0 @@ | ||||
| """authentik audit app""" | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikAuditConfig(AppConfig): | ||||
|     """authentik audit app""" | ||||
|  | ||||
|     name = "authentik.audit" | ||||
|     label = "authentik_audit" | ||||
|     verbose_name = "authentik Audit" | ||||
|     mountpoint = "audit/" | ||||
|  | ||||
|     def ready(self): | ||||
|         import_module("authentik.audit.signals") | ||||
| @ -1,199 +0,0 @@ | ||||
| """authentik audit models""" | ||||
| from inspect import getmodule, stack | ||||
| from typing import Any, Dict, Optional, Union | ||||
| from uuid import UUID, uuid4 | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.db.models.base import Model | ||||
| from django.http import HttpRequest | ||||
| from django.utils.translation import gettext as _ | ||||
| from django.views.debug import SafeExceptionReporterFilter | ||||
| from guardian.utils import get_anonymous_user | ||||
| from structlog import get_logger | ||||
|  | ||||
| from authentik.core.middleware import ( | ||||
|     SESSION_IMPERSONATE_ORIGINAL_USER, | ||||
|     SESSION_IMPERSONATE_USER, | ||||
| ) | ||||
| from authentik.core.models import User | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
|  | ||||
| LOGGER = get_logger("authentik.audit") | ||||
|  | ||||
|  | ||||
| def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||
|     """Cleanse a dictionary, recursively""" | ||||
|     final_dict = {} | ||||
|     for key, value in source.items(): | ||||
|         try: | ||||
|             if SafeExceptionReporterFilter.hidden_settings.search(key): | ||||
|                 final_dict[key] = SafeExceptionReporterFilter.cleansed_substitute | ||||
|             else: | ||||
|                 final_dict[key] = value | ||||
|         except TypeError: | ||||
|             final_dict[key] = value | ||||
|         if isinstance(value, dict): | ||||
|             final_dict[key] = cleanse_dict(value) | ||||
|     return final_dict | ||||
|  | ||||
|  | ||||
| def model_to_dict(model: Model) -> Dict[str, Any]: | ||||
|     """Convert model to dict""" | ||||
|     name = str(model) | ||||
|     if hasattr(model, "name"): | ||||
|         name = model.name | ||||
|     return { | ||||
|         "app": model._meta.app_label, | ||||
|         "model_name": model._meta.model_name, | ||||
|         "pk": model.pk, | ||||
|         "name": name, | ||||
|     } | ||||
|  | ||||
|  | ||||
| def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]: | ||||
|     """Convert user object to dictionary, optionally including the original user""" | ||||
|     if isinstance(user, AnonymousUser): | ||||
|         user = get_anonymous_user() | ||||
|     user_data = { | ||||
|         "username": user.username, | ||||
|         "pk": user.pk, | ||||
|         "email": user.email, | ||||
|     } | ||||
|     if original_user: | ||||
|         original_data = get_user(original_user) | ||||
|         original_data["on_behalf_of"] = user_data | ||||
|         return original_data | ||||
|     return user_data | ||||
|  | ||||
|  | ||||
| def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||
|     """clean source of all Models that would interfere with the JSONField. | ||||
|     Models are replaced with a dictionary of { | ||||
|         app: str, | ||||
|         name: str, | ||||
|         pk: Any | ||||
|     }""" | ||||
|     final_dict = {} | ||||
|     for key, value in source.items(): | ||||
|         if isinstance(value, dict): | ||||
|             final_dict[key] = sanitize_dict(value) | ||||
|         elif isinstance(value, models.Model): | ||||
|             final_dict[key] = sanitize_dict(model_to_dict(value)) | ||||
|         elif isinstance(value, UUID): | ||||
|             final_dict[key] = value.hex | ||||
|         else: | ||||
|             final_dict[key] = value | ||||
|     return final_dict | ||||
|  | ||||
|  | ||||
| class EventAction(models.TextChoices): | ||||
|     """All possible actions to save into the audit log""" | ||||
|  | ||||
|     LOGIN = "login" | ||||
|     LOGIN_FAILED = "login_failed" | ||||
|     LOGOUT = "logout" | ||||
|  | ||||
|     USER_WRITE = "user_write" | ||||
|     SUSPICIOUS_REQUEST = "suspicious_request" | ||||
|     PASSWORD_SET = "password_set"  # noqa # nosec | ||||
|  | ||||
|     TOKEN_VIEW = "token_view"  # nosec | ||||
|  | ||||
|     INVITE_CREATED = "invitation_created" | ||||
|     INVITE_USED = "invitation_used" | ||||
|  | ||||
|     AUTHORIZE_APPLICATION = "authorize_application" | ||||
|     SOURCE_LINKED = "source_linked" | ||||
|  | ||||
|     IMPERSONATION_STARTED = "impersonation_started" | ||||
|     IMPERSONATION_ENDED = "impersonation_ended" | ||||
|  | ||||
|     MODEL_CREATED = "model_created" | ||||
|     MODEL_UPDATED = "model_updated" | ||||
|     MODEL_DELETED = "model_deleted" | ||||
|  | ||||
|     CUSTOM_PREFIX = "custom_" | ||||
|  | ||||
|  | ||||
| class Event(models.Model): | ||||
|     """An individual audit log event""" | ||||
|  | ||||
|     event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|     user = models.JSONField(default=dict) | ||||
|     action = models.TextField(choices=EventAction.choices) | ||||
|     app = models.TextField() | ||||
|     context = models.JSONField(default=dict, blank=True) | ||||
|     client_ip = models.GenericIPAddressField(null=True) | ||||
|     created = models.DateTimeField(auto_now_add=True) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _get_app_from_request(request: HttpRequest) -> str: | ||||
|         if not isinstance(request, HttpRequest): | ||||
|             return "" | ||||
|         return request.resolver_match.app_name | ||||
|  | ||||
|     @staticmethod | ||||
|     def new( | ||||
|         action: Union[str, EventAction], | ||||
|         app: Optional[str] = None, | ||||
|         _inspect_offset: int = 1, | ||||
|         **kwargs, | ||||
|     ) -> "Event": | ||||
|         """Create new Event instance from arguments. Instance is NOT saved.""" | ||||
|         if not isinstance(action, EventAction): | ||||
|             action = EventAction.CUSTOM_PREFIX + action | ||||
|         if not app: | ||||
|             app = getmodule(stack()[_inspect_offset][0]).__name__ | ||||
|         cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs)) | ||||
|         event = Event(action=action, app=app, context=cleaned_kwargs) | ||||
|         return event | ||||
|  | ||||
|     def from_http( | ||||
|         self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None | ||||
|     ) -> "Event": | ||||
|         """Add data from a Django-HttpRequest, allowing the creation of | ||||
|         Events independently from requests. | ||||
|         `user` arguments optionally overrides user from requests.""" | ||||
|         if hasattr(request, "user"): | ||||
|             self.user = get_user( | ||||
|                 request.user, | ||||
|                 request.session.get(SESSION_IMPERSONATE_ORIGINAL_USER, None), | ||||
|             ) | ||||
|         if user: | ||||
|             self.user = get_user(user) | ||||
|         # Check if we're currently impersonating, and add that user | ||||
|         if hasattr(request, "session"): | ||||
|             if SESSION_IMPERSONATE_ORIGINAL_USER in request.session: | ||||
|                 self.user = get_user(request.session[SESSION_IMPERSONATE_ORIGINAL_USER]) | ||||
|                 self.user["on_behalf_of"] = get_user( | ||||
|                     request.session[SESSION_IMPERSONATE_USER] | ||||
|                 ) | ||||
|         # User 255.255.255.255 as fallback if IP cannot be determined | ||||
|         self.client_ip = get_client_ip(request) or "255.255.255.255" | ||||
|         # If there's no app set, we get it from the requests too | ||||
|         if not self.app: | ||||
|             self.app = Event._get_app_from_request(request) | ||||
|         self.save() | ||||
|         return self | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if not self._state.adding: | ||||
|             raise ValidationError( | ||||
|                 "you may not edit an existing %s" % self._meta.model_name | ||||
|             ) | ||||
|         LOGGER.debug( | ||||
|             "Created Audit event", | ||||
|             action=self.action, | ||||
|             context=self.context, | ||||
|             client_ip=self.client_ip, | ||||
|             user=self.user, | ||||
|         ) | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Audit Event") | ||||
|         verbose_name_plural = _("Audit Events") | ||||
| @ -1,90 +0,0 @@ | ||||
| {% extends "base/page.html" %} | ||||
|  | ||||
| {% load i18n %} | ||||
| {% load authentik_utils %} | ||||
|  | ||||
| {% block page_content %} | ||||
| <main role="main" class="pf-c-page__main" tabindex="-1" id="main-content"> | ||||
|     <section class="pf-c-page__main-section pf-m-light"> | ||||
|         <div class="pf-c-content"> | ||||
|             <h1> | ||||
|                 <i class="pf-icon pf-icon-catalog"></i> | ||||
|                 {% trans 'Audit Log' %} | ||||
|             </h1> | ||||
|         </div> | ||||
|     </section> | ||||
|     <section class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|         <div class="pf-c-card"> | ||||
|             <div class="pf-c-toolbar"> | ||||
|                 <div class="pf-c-toolbar__content"> | ||||
|                     {% include 'partials/toolbar_search.html' %} | ||||
|                     <button role="ak-refresh" class="pf-c-button pf-m-primary"> | ||||
|                         {% trans 'Refresh' %} | ||||
|                     </button> | ||||
|                     {% include 'partials/pagination.html' %} | ||||
|                 </div> | ||||
|             </div> | ||||
|             <table class="pf-c-table pf-m-compact pf-m-grid-xl" role="grid"> | ||||
|                 <thead> | ||||
|                     <tr role="row"> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Action' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Context' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'User' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Creation Date' %}</th> | ||||
|                         <th role="columnheader" scope="col">{% trans 'Client IP' %}</th> | ||||
|                     </tr> | ||||
|                 </thead> | ||||
|                 <tbody role="rowgroup"> | ||||
|                     {% for entry in object_list %} | ||||
|                     <tr role="row"> | ||||
|                         <th role="columnheader"> | ||||
|                             <div> | ||||
|                                 <div>{{ entry.action }}</div> | ||||
|                                 <small>{{ entry.app|default:'-' }}</small> | ||||
|                             </div> | ||||
|                         </th> | ||||
|                         <td role="cell"> | ||||
|                             <div> | ||||
|                                 <div> | ||||
|                                     <code>{{ entry.context }}</code> | ||||
|                                 </div> | ||||
|                                 {% if entry.user.on_behalf_of %} | ||||
|                                 <small> | ||||
|                                     {% blocktrans with username=entry.user.on_behalf_of.username %} | ||||
|                                     On behalf of {{ username }} | ||||
|                                     {% endblocktrans %} | ||||
|                                 </small> | ||||
|                                 {% endif %} | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td role="cell"> | ||||
|                             <div> | ||||
|                                 <div>{{ entry.user.username }}</div> | ||||
|                                 <small> | ||||
|                                     {% blocktrans with pk=entry.user.pk %} | ||||
|                                     ID: {{ pk }} | ||||
|                                     {% endblocktrans %} | ||||
|                                 </small> | ||||
|                             </div> | ||||
|                         </td> | ||||
|                         <td role="cell"> | ||||
|                             <span> | ||||
|                                 {{ entry.created }} | ||||
|                             </span> | ||||
|                         </td> | ||||
|                         <td role="cell"> | ||||
|                             <span> | ||||
|                                 {{ entry.client_ip }} | ||||
|                             </span> | ||||
|                         </td> | ||||
|                     </tr> | ||||
|                     {% endfor %} | ||||
|                 </tbody> | ||||
|             </table> | ||||
|             <div class="pf-c-pagination pf-m-bottom"> | ||||
|                 {% include 'partials/pagination.html' %} | ||||
|             </div> | ||||
|         </div> | ||||
|     </section> | ||||
| </main> | ||||
| {% endblock %} | ||||
| @ -1,9 +0,0 @@ | ||||
| """authentik audit urls""" | ||||
| from django.urls import path | ||||
|  | ||||
| from authentik.audit.views import EventListView | ||||
|  | ||||
| urlpatterns = [ | ||||
|     # Audit Log | ||||
|     path("audit/", EventListView.as_view(), name="log"), | ||||
| ] | ||||
| @ -1,30 +0,0 @@ | ||||
| """authentik Event administration""" | ||||
| from django.contrib.auth.mixins import LoginRequiredMixin | ||||
| from django.views.generic import ListView | ||||
| from guardian.mixins import PermissionListMixin | ||||
|  | ||||
| from authentik.admin.views.utils import SearchListMixin, UserPaginateListMixin | ||||
| from authentik.audit.models import Event | ||||
|  | ||||
|  | ||||
| class EventListView( | ||||
|     PermissionListMixin, | ||||
|     LoginRequiredMixin, | ||||
|     SearchListMixin, | ||||
|     UserPaginateListMixin, | ||||
|     ListView, | ||||
| ): | ||||
|     """Show list of all invitations""" | ||||
|  | ||||
|     model = Event | ||||
|     template_name = "audit/list.html" | ||||
|     permission_required = "authentik_audit.view_event" | ||||
|     ordering = "-created" | ||||
|  | ||||
|     search_fields = [ | ||||
|         "user", | ||||
|         "action", | ||||
|         "app", | ||||
|         "context", | ||||
|         "client_ip", | ||||
|     ] | ||||
| @ -4,7 +4,7 @@ from django.apps import AppConfig, apps | ||||
| from django.contrib import admin | ||||
| from django.contrib.admin.sites import AlreadyRegistered | ||||
| from guardian.admin import GuardedModelAdmin | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| """Application API Views""" | ||||
| from django.core.cache import cache | ||||
| from django.db.models import QuerySet | ||||
| from django.http.response import Http404 | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| @ -12,12 +13,17 @@ from rest_framework.viewsets import ModelViewSet | ||||
| from rest_framework_guardian.filters import ObjectPermissionsFilter | ||||
|  | ||||
| from authentik.admin.api.metrics import get_events_per_1h | ||||
| from authentik.audit.models import EventAction | ||||
| from authentik.core.api.providers import ProviderSerializer | ||||
| from authentik.core.models import Application | ||||
| from authentik.events.models import EventAction | ||||
| from authentik.policies.engine import PolicyEngine | ||||
|  | ||||
|  | ||||
| def user_app_cache_key(user_pk: str) -> str: | ||||
|     """Cache key where application list for user is saved""" | ||||
|     return f"user_app_cache_{user_pk}" | ||||
|  | ||||
|  | ||||
| class ApplicationSerializer(ModelSerializer): | ||||
|     """Application Serializer""" | ||||
|  | ||||
| @ -50,7 +56,15 @@ class ApplicationViewSet(ModelViewSet): | ||||
|  | ||||
|     queryset = Application.objects.all() | ||||
|     serializer_class = ApplicationSerializer | ||||
|     search_fields = [ | ||||
|         "name", | ||||
|         "slug", | ||||
|         "meta_launch_url", | ||||
|         "meta_description", | ||||
|         "meta_publisher", | ||||
|     ] | ||||
|     lookup_field = "slug" | ||||
|     ordering = ["name"] | ||||
|  | ||||
|     def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet: | ||||
|         """Custom filter_queryset method which ignores guardian, but still supports sorting""" | ||||
| @ -64,12 +78,15 @@ class ApplicationViewSet(ModelViewSet): | ||||
|         """Custom list method that checks Policy based access instead of guardian""" | ||||
|         queryset = self._filter_queryset_for_list(self.get_queryset()) | ||||
|         self.paginate_queryset(queryset) | ||||
|         allowed_applications = [] | ||||
|         for application in queryset.order_by("name"): | ||||
|             engine = PolicyEngine(application, self.request.user, self.request) | ||||
|             engine.build() | ||||
|             if engine.passing: | ||||
|                 allowed_applications.append(application) | ||||
|         allowed_applications = cache.get(user_app_cache_key(self.request.user.pk)) | ||||
|         if not allowed_applications: | ||||
|             allowed_applications = [] | ||||
|             for application in queryset: | ||||
|                 engine = PolicyEngine(application, self.request.user, self.request) | ||||
|                 engine.build() | ||||
|                 if engine.passing: | ||||
|                     allowed_applications.append(application) | ||||
|             cache.set(user_app_cache_key(self.request.user.pk), allowed_applications) | ||||
|         serializer = self.get_serializer(allowed_applications, many=True) | ||||
|         return self.get_paginated_response(serializer.data) | ||||
|  | ||||
| @ -80,7 +97,7 @@ class ApplicationViewSet(ModelViewSet): | ||||
|             get_objects_for_user(request.user, "authentik_core.view_application"), | ||||
|             slug=slug, | ||||
|         ) | ||||
|         if not request.user.has_perm("authentik_audit.view_event"): | ||||
|         if not request.user.has_perm("authentik_events.view_event"): | ||||
|             raise Http404 | ||||
|         return Response( | ||||
|             get_events_per_1h( | ||||
|  | ||||
| @ -23,7 +23,7 @@ class PropertyMappingSerializer(ModelSerializer): | ||||
| class PropertyMappingViewSet(ReadOnlyModelViewSet): | ||||
|     """PropertyMapping Viewset""" | ||||
|  | ||||
|     queryset = PropertyMapping.objects.all() | ||||
|     queryset = PropertyMapping.objects.none() | ||||
|     serializer_class = PropertyMappingSerializer | ||||
|  | ||||
|     def get_queryset(self): | ||||
|  | ||||
| @ -2,15 +2,16 @@ | ||||
| from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.core.api.utils import MetaNameSerializer | ||||
| from authentik.core.models import Provider | ||||
|  | ||||
|  | ||||
| class ProviderSerializer(ModelSerializer): | ||||
| class ProviderSerializer(ModelSerializer, MetaNameSerializer): | ||||
|     """Provider Serializer""" | ||||
|  | ||||
|     __type__ = SerializerMethodField(method_name="get_type") | ||||
|     object_type = SerializerMethodField() | ||||
|  | ||||
|     def get_type(self, obj): | ||||
|     def get_object_type(self, obj): | ||||
|         """Get object type so that we know which API Endpoint to use to get the full object""" | ||||
|         return obj._meta.object_name.lower().replace("provider", "") | ||||
|  | ||||
| @ -29,14 +30,16 @@ class ProviderSerializer(ModelSerializer): | ||||
|             "application", | ||||
|             "authorization_flow", | ||||
|             "property_mappings", | ||||
|             "__type__", | ||||
|             "object_type", | ||||
|             "verbose_name", | ||||
|             "verbose_name_plural", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class ProviderViewSet(ModelViewSet): | ||||
|     """Provider Viewset""" | ||||
|  | ||||
|     queryset = Provider.objects.all() | ||||
|     queryset = Provider.objects.none() | ||||
|     serializer_class = ProviderSerializer | ||||
|     filterset_fields = { | ||||
|         "application": ["isnull"], | ||||
|  | ||||
| @ -3,10 +3,11 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
|  | ||||
| from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS | ||||
| from authentik.core.api.utils import MetaNameSerializer | ||||
| from authentik.core.models import Source | ||||
|  | ||||
|  | ||||
| class SourceSerializer(ModelSerializer): | ||||
| class SourceSerializer(ModelSerializer, MetaNameSerializer): | ||||
|     """Source Serializer""" | ||||
|  | ||||
|     __type__ = SerializerMethodField(method_name="get_type") | ||||
| @ -30,7 +31,7 @@ class SourceSerializer(ModelSerializer): | ||||
| class SourceViewSet(ReadOnlyModelViewSet): | ||||
|     """Source Viewset""" | ||||
|  | ||||
|     queryset = Source.objects.all() | ||||
|     queryset = Source.objects.none() | ||||
|     serializer_class = SourceSerializer | ||||
|     lookup_field = "slug" | ||||
|  | ||||
|  | ||||
| @ -6,8 +6,8 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.audit.models import Event, EventAction | ||||
| from authentik.core.models import Token | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| class TokenSerializer(ModelSerializer): | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| """User API Views""" | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| from guardian.utils import get_anonymous_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| @ -33,9 +34,12 @@ class UserSerializer(ModelSerializer): | ||||
| class UserViewSet(ModelViewSet): | ||||
|     """User Viewset""" | ||||
|  | ||||
|     queryset = User.objects.all() | ||||
|     queryset = User.objects.none() | ||||
|     serializer_class = UserSerializer | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         return User.objects.all().exclude(pk=get_anonymous_user().pk) | ||||
|  | ||||
|     @swagger_auto_schema(responses={200: UserSerializer(many=False)}) | ||||
|     @action(detail=False) | ||||
|     # pylint: disable=invalid-name | ||||
|  | ||||
							
								
								
									
										24
									
								
								authentik/core/api/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								authentik/core/api/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| """API Utilities""" | ||||
| from django.db.models import Model | ||||
| from rest_framework.serializers import Serializer, SerializerMethodField | ||||
|  | ||||
|  | ||||
| class MetaNameSerializer(Serializer): | ||||
|     """Add verbose names to response""" | ||||
|  | ||||
|     verbose_name = SerializerMethodField() | ||||
|     verbose_name_plural = SerializerMethodField() | ||||
|  | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def get_verbose_name(self, obj: Model) -> str: | ||||
|         """Return object's verbose_name""" | ||||
|         return obj._meta.verbose_name | ||||
|  | ||||
|     def get_verbose_name_plural(self, obj: Model) -> str: | ||||
|         """Return object's plural verbose_name""" | ||||
|         return obj._meta.verbose_name_plural | ||||
| @ -1,7 +1,7 @@ | ||||
| """Channels base classes""" | ||||
| from channels.exceptions import DenyConnection | ||||
| from channels.generic.websocket import JsonWebsocketConsumer | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.api.auth import token_from_header | ||||
| from authentik.core.models import User | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| """Property Mapping Evaluator""" | ||||
| from traceback import format_tb | ||||
| from typing import Optional | ||||
|  | ||||
| from django.http import HttpRequest | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.lib.expression.evaluator import BaseEvaluator | ||||
|  | ||||
|  | ||||
| @ -19,3 +21,18 @@ class PropertyMappingEvaluator(BaseEvaluator): | ||||
|         if request: | ||||
|             self._context["request"] = request | ||||
|         self._context.update(**kwargs) | ||||
|  | ||||
|     def handle_error(self, exc: Exception, expression_source: str): | ||||
|         """Exception Handler""" | ||||
|         error_string = "\n".join(format_tb(exc.__traceback__) + [str(exc)]) | ||||
|         event = Event.new( | ||||
|             EventAction.PROPERTY_MAPPING_EXCEPTION, | ||||
|             expression=expression_source, | ||||
|             message=error_string, | ||||
|         ) | ||||
|         if "user" in self._context: | ||||
|             event.set_user(self._context["user"]) | ||||
|         if "request" in self._context: | ||||
|             event.from_http(self._context["request"]) | ||||
|             return | ||||
|         event.save() | ||||
|  | ||||
| @ -15,7 +15,7 @@ from django.utils.translation import gettext_lazy as _ | ||||
| from guardian.mixins import GuardianUserMixin | ||||
| from model_utils.managers import InheritanceManager | ||||
| from rest_framework.serializers import Serializer | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.exceptions import PropertyMappingExpressionException | ||||
| from authentik.core.signals import password_changed | ||||
|  | ||||
| @ -8,7 +8,7 @@ from dbbackup.db.exceptions import CommandConnectorError | ||||
| from django.contrib.humanize.templatetags.humanize import naturaltime | ||||
| from django.core import management | ||||
| from django.utils.timezone import now | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import ExpiringModel | ||||
| from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
|  | ||||
| @ -9,14 +9,14 @@ | ||||
|         <meta charset="UTF-8"> | ||||
|         <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> | ||||
|         <title>{% block title %}{% trans title|default:config.authentik.branding.title %}{% endblock %}</title> | ||||
|         <link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}"> | ||||
|         <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}"> | ||||
|         <script src="{% url 'javascript-catalog' %}"></script> | ||||
|         <script src="{% static 'dist/main.js' %}" type="module"></script> | ||||
|         <link rel="icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> | ||||
|         <link rel="shortcut icon" type="image/png" href="{% static 'dist/assets/icons/icon.png' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/patternfly-addons.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/fontawesome.min.css' %}?v={{ ak_version }}"> | ||||
|         <link rel="stylesheet" type="text/css" href="{% static 'dist/authentik.css' %}?v={{ ak_version }}"> | ||||
|         <script src="{% url 'javascript-catalog' %}?v={{ ak_version }}"></script> | ||||
|         <script src="{% static 'dist/main.js' %}?v={{ ak_version }}" type="module"></script> | ||||
|         {% block head %} | ||||
|         {% endblock %} | ||||
|     </head> | ||||
|  | ||||
| @ -31,7 +31,7 @@ | ||||
|             <p class="pf-c-form__helper-text">{{ field.help_text }}</p> | ||||
|             {% endif %} | ||||
|         </div> | ||||
|     {% elif field.field.widget|fieldtype == 'Select' %} | ||||
|     {% elif field.field.widget|fieldtype == 'Select' or field.field.widget|fieldtype == "SelectMultiple" %} | ||||
|         <div class="pf-c-form__group-label"> | ||||
|             <label class="pf-c-form__label" for="{{ field.name }}-{{ forloop.counter0 }}"> | ||||
|                 <span class="pf-c-form__label-text">{{ field.label }}</span> | ||||
| @ -46,6 +46,9 @@ | ||||
|                 {% if field.help_text %} | ||||
|                 <p class="pf-c-form__helper-text">{{ field.help_text|safe }}</p> | ||||
|                 {% endif %} | ||||
|                 {% if field.field.widget|fieldtype == 'SelectMultiple' %} | ||||
|                 <p class="pf-c-form__helper-text">{% trans 'Hold control/command to select multiple items.' %}</p> | ||||
|                 {% endif %} | ||||
|             </div> | ||||
|         </div> | ||||
|     {% elif field.field.widget|fieldtype == 'CheckboxInput' %} | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| {% load i18n %} | ||||
| {% load authentik_user_settings %} | ||||
| {% load authentik_utils %} | ||||
|  | ||||
| <div class="pf-c-page"> | ||||
|     <main role="main" class="pf-c-page__main" tabindex="-1"> | ||||
| @ -12,47 +13,45 @@ | ||||
|                 <p>{% trans "Configure settings relevant to your user profile." %}</p> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section class="pf-c-page__main-section"> | ||||
|             <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                 <div class="pf-u-w-75"> | ||||
|                     <ak-site-shell url="{% url 'authentik_core:user-details' %}"> | ||||
|                         <div slot="body"></div> | ||||
|                     </ak-site-shell> | ||||
|         <ak-tabs> | ||||
|             <section slot="page-1" data-tab-title="{% trans 'User details' %}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                 <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                     <div class="pf-u-w-75"> | ||||
|                         <ak-site-shell url="{% url 'authentik_core:user-details' %}"> | ||||
|                             <div slot="body"></div> | ||||
|                         </ak-site-shell> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </section> | ||||
|         <section class="pf-c-page__main-section"> | ||||
|             <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                 <div class="pf-u-w-75"> | ||||
|                     <ak-site-shell url="{% url 'authentik_core:user-tokens' %}"> | ||||
|                         <div slot="body"></div> | ||||
|                     </ak-site-shell> | ||||
|             </section> | ||||
|             <section slot="page-2" data-tab-title="{% trans 'Tokens' %}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                 <ak-site-shell url="{% url 'authentik_core:user-tokens' %}"> | ||||
|                     <div slot="body"></div> | ||||
|                 </ak-site-shell> | ||||
|             </section> | ||||
|             {% user_stages as user_stages_loc %} | ||||
|             {% for stage, stage_link in user_stages_loc.items %} | ||||
|             <section slot="page-{{ stage.pk }}" data-tab-title="{{ stage|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                 <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                     <div class="pf-u-w-75"> | ||||
|                         <ak-site-shell url="{{ stage_link }}"> | ||||
|                             <div slot="body"></div> | ||||
|                         </ak-site-shell> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </section> | ||||
|         {% user_stages as user_stages_loc %} | ||||
|         {% for stage in user_stages_loc %} | ||||
|         <section class="pf-c-page__main-section"> | ||||
|             <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                 <div class="pf-u-w-75"> | ||||
|                     <ak-site-shell url="{{ stage }}"> | ||||
|                         <div slot="body"></div> | ||||
|                     </ak-site-shell> | ||||
|             </section> | ||||
|             {% endfor %} | ||||
|             {% user_sources as user_sources_loc %} | ||||
|             {% for source, source_link in user_sources_loc.item %} | ||||
|             <section slot="page-{{ source.pk }}" data-tab-title="{{ source|verbose_name }}" class="pf-c-page__main-section pf-m-no-padding-mobile"> | ||||
|                 <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                     <div class="pf-u-w-75"> | ||||
|                         <ak-site-shell url="{{ source_link }}"> | ||||
|                             <div slot="body"></div> | ||||
|                         </ak-site-shell> | ||||
|                     </div> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </section> | ||||
|         {% endfor %} | ||||
|         {% user_sources as user_sources_loc %} | ||||
|         {% for source in user_sources_loc %} | ||||
|         <section class="pf-c-page__main-section"> | ||||
|             <div class="pf-u-display-flex pf-u-justify-content-center"> | ||||
|                 <div class="pf-u-w-75"> | ||||
|                     <ak-site-shell url="{{ source }}"> | ||||
|                         <div slot="body"></div> | ||||
|                     </ak-site-shell> | ||||
|                 </div> | ||||
|             </div> | ||||
|         </section> | ||||
|         {% endfor %} | ||||
|             </section> | ||||
|             {% endfor %} | ||||
|         </ak-tabs> | ||||
|     </main> | ||||
| </div> | ||||
|  | ||||
| @ -13,26 +13,26 @@ register = template.Library() | ||||
|  | ||||
| @register.simple_tag(takes_context=True) | ||||
| # pylint: disable=unused-argument | ||||
| def user_stages(context: RequestContext) -> list[str]: | ||||
| def user_stages(context: RequestContext) -> dict[Stage, str]: | ||||
|     """Return list of all stages which apply to user""" | ||||
|     _all_stages: Iterable[Stage] = Stage.objects.all().select_subclasses() | ||||
|     matching_stages: list[str] = [] | ||||
|     matching_stages: dict[Stage, str] = {} | ||||
|     for stage in _all_stages: | ||||
|         user_settings = stage.ui_user_settings | ||||
|         if not user_settings: | ||||
|             continue | ||||
|         matching_stages.append(user_settings) | ||||
|         matching_stages[stage] = user_settings | ||||
|     return matching_stages | ||||
|  | ||||
|  | ||||
| @register.simple_tag(takes_context=True) | ||||
| def user_sources(context: RequestContext) -> list[str]: | ||||
| def user_sources(context: RequestContext) -> dict[Source, str]: | ||||
|     """Return a list of all sources which are enabled for the user""" | ||||
|     user = context.get("request").user | ||||
|     _all_sources: Iterable[Source] = Source.objects.filter( | ||||
|         enabled=True | ||||
|     ).select_subclasses() | ||||
|     matching_sources: list[str] = [] | ||||
|     matching_sources: dict[Source, str] = {} | ||||
|     for source in _all_sources: | ||||
|         user_settings = source.ui_user_settings | ||||
|         if not user_settings: | ||||
| @ -40,5 +40,5 @@ def user_sources(context: RequestContext) -> list[str]: | ||||
|         policy_engine = PolicyEngine(source, user, context.get("request")) | ||||
|         policy_engine.build() | ||||
|         if policy_engine.passing: | ||||
|             matching_sources.append(user_settings) | ||||
|             matching_sources[source] = user_settings | ||||
|     return matching_sources | ||||
|  | ||||
							
								
								
									
										56
									
								
								authentik/core/tests/test_property_mapping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								authentik/core/tests/test_property_mapping.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,56 @@ | ||||
| """authentik core property mapping tests""" | ||||
| from django.test import RequestFactory, TestCase | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
|  | ||||
| from authentik.core.exceptions import PropertyMappingExpressionException | ||||
| from authentik.core.models import PropertyMapping | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| class TestPropertyMappings(TestCase): | ||||
|     """authentik core property mapping tests""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.factory = RequestFactory() | ||||
|  | ||||
|     def test_expression(self): | ||||
|         """Test expression""" | ||||
|         mapping = PropertyMapping.objects.create( | ||||
|             name="test", expression="return 'test'" | ||||
|         ) | ||||
|         self.assertEqual(mapping.evaluate(None, None), "test") | ||||
|  | ||||
|     def test_expression_syntax(self): | ||||
|         """Test expression syntax error""" | ||||
|         mapping = PropertyMapping.objects.create(name="test", expression="-") | ||||
|         with self.assertRaises(PropertyMappingExpressionException): | ||||
|             mapping.evaluate(None, None) | ||||
|  | ||||
|     def test_expression_error_general(self): | ||||
|         """Test expression error""" | ||||
|         expr = "return aaa" | ||||
|         mapping = PropertyMapping.objects.create(name="test", expression=expr) | ||||
|         with self.assertRaises(NameError): | ||||
|             mapping.evaluate(None, None) | ||||
|         events = Event.objects.filter( | ||||
|             action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr | ||||
|         ) | ||||
|         self.assertTrue(events.exists()) | ||||
|         self.assertEqual(len(events), 1) | ||||
|  | ||||
|     def test_expression_error_extended(self): | ||||
|         """Test expression error (with user and http request""" | ||||
|         expr = "return aaa" | ||||
|         request = self.factory.get("/") | ||||
|         mapping = PropertyMapping.objects.create(name="test", expression=expr) | ||||
|         with self.assertRaises(NameError): | ||||
|             mapping.evaluate(get_anonymous_user(), request) | ||||
|         events = Event.objects.filter( | ||||
|             action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr | ||||
|         ) | ||||
|         self.assertTrue(events.exists()) | ||||
|         self.assertEqual(len(events), 1) | ||||
|         event = events.first() | ||||
|         self.assertEqual(event.user["username"], "AnonymousUser") | ||||
|         self.assertEqual(event.client_ip, "127.0.0.1") | ||||
| @ -25,7 +25,7 @@ urlpatterns = [ | ||||
|         name="user-tokens-delete", | ||||
|     ), | ||||
|     # Libray | ||||
|     path("library/", library.LibraryView.as_view(), name="overview"), | ||||
|     path("library", library.LibraryView.as_view(), name="overview"), | ||||
|     # Impersonation | ||||
|     path( | ||||
|         "-/impersonation/<int:user_id>/", | ||||
|  | ||||
| @ -3,14 +3,14 @@ | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from django.shortcuts import get_object_or_404, redirect | ||||
| from django.views import View | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.audit.models import Event, EventAction | ||||
| from authentik.core.middleware import ( | ||||
|     SESSION_IMPERSONATE_ORIGINAL_USER, | ||||
|     SESSION_IMPERSONATE_USER, | ||||
| ) | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @ -94,11 +94,6 @@ class TokenCreateView( | ||||
|     success_url = reverse_lazy("authentik_core:user-tokens") | ||||
|     success_message = _("Successfully created Token") | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["container_template"] = "user/base.html" | ||||
|         return kwargs | ||||
|  | ||||
|     def form_valid(self, form: UserTokenForm) -> HttpResponse: | ||||
|         form.instance.user = self.request.user | ||||
|         form.instance.intent = TokenIntents.INTENT_API | ||||
| @ -112,21 +107,20 @@ class TokenUpdateView( | ||||
|  | ||||
|     model = Token | ||||
|     form_class = UserTokenForm | ||||
|     permission_required = "authentik_core.update_token" | ||||
|     permission_required = "authentik_core.change_token" | ||||
|     template_name = "generic/update.html" | ||||
|     success_url = reverse_lazy("authentik_core:user-tokens") | ||||
|     success_message = _("Successfully updated Token") | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["container_template"] = "user/base.html" | ||||
|         return kwargs | ||||
|  | ||||
|     def get_object(self) -> Token: | ||||
|         identifier = self.kwargs.get("identifier") | ||||
|         return get_objects_for_user( | ||||
|             self.request.user, "authentik_core.update_token", self.model | ||||
|         ).filter(intent=TokenIntents.INTENT_API, identifier=identifier) | ||||
|         return ( | ||||
|             get_objects_for_user( | ||||
|                 self.request.user, self.permission_required, self.model | ||||
|             ) | ||||
|             .filter(intent=TokenIntents.INTENT_API, identifier=identifier) | ||||
|             .first() | ||||
|         ) | ||||
|  | ||||
|  | ||||
| class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView): | ||||
| @ -138,7 +132,12 @@ class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage | ||||
|     success_url = reverse_lazy("authentik_core:user-tokens") | ||||
|     success_message = _("Successfully deleted Token") | ||||
|  | ||||
|     def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: | ||||
|         kwargs = super().get_context_data(**kwargs) | ||||
|         kwargs["container_template"] = "user/base.html" | ||||
|         return kwargs | ||||
|     def get_object(self) -> Token: | ||||
|         identifier = self.kwargs.get("identifier") | ||||
|         return ( | ||||
|             get_objects_for_user( | ||||
|                 self.request.user, self.permission_required, self.model | ||||
|             ) | ||||
|             .filter(intent=TokenIntents.INTENT_API, identifier=identifier) | ||||
|             .first() | ||||
|         ) | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| """Audit API Views""" | ||||
| """Events API Views""" | ||||
| from django.db.models.aggregates import Count | ||||
| from django.db.models.fields.json import KeyTextTransform | ||||
| from drf_yasg2.utils import swagger_auto_schema | ||||
| @ -9,7 +9,7 @@ from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer, Serializer | ||||
| from rest_framework.viewsets import ReadOnlyModelViewSet | ||||
| 
 | ||||
| from authentik.audit.models import Event, EventAction | ||||
| from authentik.events.models import Event, EventAction | ||||
| 
 | ||||
| 
 | ||||
| class EventSerializer(ModelSerializer): | ||||
| @ -48,6 +48,15 @@ class EventViewSet(ReadOnlyModelViewSet): | ||||
| 
 | ||||
|     queryset = Event.objects.all() | ||||
|     serializer_class = EventSerializer | ||||
|     ordering = ["-created"] | ||||
|     search_fields = [ | ||||
|         "user", | ||||
|         "action", | ||||
|         "app", | ||||
|         "context", | ||||
|         "client_ip", | ||||
|     ] | ||||
|     filterset_fields = ["action"] | ||||
| 
 | ||||
|     @swagger_auto_schema( | ||||
|         method="GET", responses={200: EventTopPerUserSerialier(many=True)} | ||||
							
								
								
									
										53
									
								
								authentik/events/api/notification.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								authentik/events/api/notification.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,53 @@ | ||||
| """Notification API Views""" | ||||
| from rest_framework import mixins | ||||
| from rest_framework.fields import ReadOnlyField | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import GenericViewSet | ||||
|  | ||||
| from authentik.events.api.event import EventSerializer | ||||
| from authentik.events.models import Notification | ||||
|  | ||||
|  | ||||
| class NotificationSerializer(ModelSerializer): | ||||
|     """Notification Serializer""" | ||||
|  | ||||
|     body = ReadOnlyField() | ||||
|     severity = ReadOnlyField() | ||||
|     event = EventSerializer() | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = Notification | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "severity", | ||||
|             "body", | ||||
|             "created", | ||||
|             "event", | ||||
|             "seen", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class NotificationViewSet( | ||||
|     mixins.RetrieveModelMixin, | ||||
|     mixins.UpdateModelMixin, | ||||
|     mixins.DestroyModelMixin, | ||||
|     mixins.ListModelMixin, | ||||
|     GenericViewSet, | ||||
| ): | ||||
|     """Notification Viewset""" | ||||
|  | ||||
|     queryset = Notification.objects.all() | ||||
|     serializer_class = NotificationSerializer | ||||
|     filterset_fields = [ | ||||
|         "severity", | ||||
|         "body", | ||||
|         "created", | ||||
|         "event", | ||||
|         "seen", | ||||
|     ] | ||||
|  | ||||
|     def get_queryset(self): | ||||
|         if not self.request: | ||||
|             return super().get_queryset() | ||||
|         return Notification.objects.filter(user=self.request.user) | ||||
							
								
								
									
										28
									
								
								authentik/events/api/notification_rule.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								authentik/events/api/notification_rule.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| """NotificationRule API Views""" | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.events.models import NotificationRule | ||||
|  | ||||
|  | ||||
| class NotificationRuleSerializer(ModelSerializer): | ||||
|     """NotificationRule Serializer""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = NotificationRule | ||||
|         depth = 2 | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "transports", | ||||
|             "severity", | ||||
|             "group", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class NotificationRuleViewSet(ModelViewSet): | ||||
|     """NotificationRule Viewset""" | ||||
|  | ||||
|     queryset = NotificationRule.objects.all() | ||||
|     serializer_class = NotificationRuleSerializer | ||||
							
								
								
									
										66
									
								
								authentik/events/api/notification_transport.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								authentik/events/api/notification_transport.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| """NotificationTransport API Views""" | ||||
| from django.http.response import Http404 | ||||
| from guardian.shortcuts import get_objects_for_user | ||||
| from rest_framework.decorators import action | ||||
| from rest_framework.fields import SerializerMethodField | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ModelSerializer | ||||
| from rest_framework.viewsets import ModelViewSet | ||||
|  | ||||
| from authentik.events.models import ( | ||||
|     Notification, | ||||
|     NotificationSeverity, | ||||
|     NotificationTransport, | ||||
|     NotificationTransportError, | ||||
|     TransportMode, | ||||
| ) | ||||
|  | ||||
|  | ||||
| class NotificationTransportSerializer(ModelSerializer): | ||||
|     """NotificationTransport Serializer""" | ||||
|  | ||||
|     mode_verbose = SerializerMethodField() | ||||
|  | ||||
|     def get_mode_verbose(self, instance: NotificationTransport): | ||||
|         """Return selected mode with a UI Label""" | ||||
|         return TransportMode(instance.mode).label | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = NotificationTransport | ||||
|         fields = [ | ||||
|             "pk", | ||||
|             "name", | ||||
|             "mode", | ||||
|             "mode_verbose", | ||||
|             "webhook_url", | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class NotificationTransportViewSet(ModelViewSet): | ||||
|     """NotificationTransport Viewset""" | ||||
|  | ||||
|     queryset = NotificationTransport.objects.all() | ||||
|     serializer_class = NotificationTransportSerializer | ||||
|  | ||||
|     @action(detail=True, methods=["post"]) | ||||
|     # pylint: disable=invalid-name | ||||
|     def test(self, request: Request, pk=None) -> Response: | ||||
|         """Send example notification using selected transport. Requires | ||||
|         Modify permissions.""" | ||||
|         transports = get_objects_for_user( | ||||
|             request.user, "authentik_events.change_notificationtransport" | ||||
|         ).filter(pk=pk) | ||||
|         if not transports.exists(): | ||||
|             raise Http404 | ||||
|         transport: NotificationTransport = transports.first() | ||||
|         notification = Notification( | ||||
|             severity=NotificationSeverity.NOTICE, | ||||
|             body=f"Test Notification from transport {transport.name}", | ||||
|             user=request.user, | ||||
|         ) | ||||
|         try: | ||||
|             return Response(transport.send(notification)) | ||||
|         except NotificationTransportError as exc: | ||||
|             return Response(str(exc.__cause__ or None), status=503) | ||||
							
								
								
									
										15
									
								
								authentik/events/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								authentik/events/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| """authentik events app""" | ||||
| from importlib import import_module | ||||
|  | ||||
| from django.apps import AppConfig | ||||
|  | ||||
|  | ||||
| class AuthentikEventsConfig(AppConfig): | ||||
|     """authentik events app""" | ||||
|  | ||||
|     name = "authentik.events" | ||||
|     label = "authentik_events" | ||||
|     verbose_name = "authentik Events" | ||||
|  | ||||
|     def ready(self): | ||||
|         import_module("authentik.events.signals") | ||||
							
								
								
									
										47
									
								
								authentik/events/forms.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								authentik/events/forms.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | ||||
| """authentik events NotificationTransport forms""" | ||||
| from django import forms | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
|  | ||||
| from authentik.events.models import NotificationRule, NotificationTransport | ||||
|  | ||||
|  | ||||
| class NotificationTransportForm(forms.ModelForm): | ||||
|     """NotificationTransport Form""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = NotificationTransport | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "mode", | ||||
|             "webhook_url", | ||||
|         ] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|             "webhook_url": forms.TextInput(), | ||||
|         } | ||||
|         labels = { | ||||
|             "webhook_url": _("Webhook URL"), | ||||
|         } | ||||
|         help_texts = { | ||||
|             "webhook_url": _( | ||||
|                 ("Only required when the Generic or Slack Webhook is used.") | ||||
|             ), | ||||
|         } | ||||
|  | ||||
|  | ||||
| class NotificationRuleForm(forms.ModelForm): | ||||
|     """NotificationRule Form""" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         model = NotificationRule | ||||
|         fields = [ | ||||
|             "name", | ||||
|             "group", | ||||
|             "transports", | ||||
|             "severity", | ||||
|         ] | ||||
|         widgets = { | ||||
|             "name": forms.TextInput(), | ||||
|         } | ||||
| @ -1,4 +1,4 @@ | ||||
| """Audit middleware""" | ||||
| """Events middleware""" | ||||
| from functools import partial | ||||
| from typing import Callable | ||||
| 
 | ||||
| @ -6,10 +6,12 @@ from django.contrib.auth.models import User | ||||
| from django.db.models import Model | ||||
| from django.db.models.signals import post_save, pre_delete | ||||
| from django.http import HttpRequest, HttpResponse | ||||
| from guardian.models import UserObjectPermission | ||||
| 
 | ||||
| from authentik.audit.models import Event, EventAction, model_to_dict | ||||
| from authentik.audit.signals import EventNewThread | ||||
| from authentik.core.middleware import LOCAL | ||||
| from authentik.events.models import Event, EventAction, Notification | ||||
| from authentik.events.signals import EventNewThread | ||||
| from authentik.events.utils import model_to_dict | ||||
| 
 | ||||
| 
 | ||||
| class AuditMiddleware: | ||||
| @ -62,7 +64,7 @@ class AuditMiddleware: | ||||
|         user: User, request: HttpRequest, sender, instance: Model, created: bool, **_ | ||||
|     ): | ||||
|         """Signal handler for all object's post_save""" | ||||
|         if isinstance(instance, Event): | ||||
|         if isinstance(instance, (Event, Notification, UserObjectPermission)): | ||||
|             return | ||||
| 
 | ||||
|         action = EventAction.MODEL_CREATED if created else EventAction.MODEL_UPDATED | ||||
| @ -74,7 +76,7 @@ class AuditMiddleware: | ||||
|         user: User, request: HttpRequest, sender, instance: Model, **_ | ||||
|     ): | ||||
|         """Signal handler for all object's pre_delete""" | ||||
|         if isinstance(instance, Event): | ||||
|         if isinstance(instance, (Event, Notification, UserObjectPermission)): | ||||
|             return | ||||
| 
 | ||||
|         EventNewThread( | ||||
| @ -63,8 +63,8 @@ class Migration(migrations.Migration): | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Audit Event", | ||||
|                 "verbose_name_plural": "Audit Events", | ||||
|                 "verbose_name": "Event", | ||||
|                 "verbose_name_plural": "Events", | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -6,7 +6,7 @@ from django.db import migrations, models | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ("authentik_audit", "0001_initial"), | ||||
|         ("authentik_events", "0001_initial"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
| @ -3,11 +3,11 @@ from django.apps.registry import Apps | ||||
| from django.db import migrations, models | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
| 
 | ||||
| import authentik.audit.models | ||||
| import authentik.events.models | ||||
| 
 | ||||
| 
 | ||||
| def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     Event = apps.get_model("authentik_audit", "Event") | ||||
|     Event = apps.get_model("authentik_events", "Event") | ||||
| 
 | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     for event in Event.objects.all(): | ||||
| @ -15,7 +15,7 @@ def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|         # Because event objects cannot be updated, we have to re-create them | ||||
|         event.pk = None | ||||
|         event.user_json = ( | ||||
|             authentik.audit.models.get_user(event.user) if event.user else {} | ||||
|             authentik.events.models.get_user(event.user) if event.user else {} | ||||
|         ) | ||||
|         event._state.adding = True | ||||
|         event.save() | ||||
| @ -24,7 +24,7 @@ def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ("authentik_audit", "0002_auto_20200918_2116"), | ||||
|         ("authentik_events", "0002_auto_20200918_2116"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
| @ -6,7 +6,7 @@ from django.db import migrations, models | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ("authentik_audit", "0003_auto_20200917_1155"), | ||||
|         ("authentik_events", "0003_auto_20200917_1155"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
| @ -6,7 +6,7 @@ from django.db import migrations, models | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ("authentik_audit", "0004_auto_20200921_1829"), | ||||
|         ("authentik_events", "0004_auto_20200921_1829"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
| @ -6,7 +6,7 @@ from django.db import migrations, models | ||||
| class Migration(migrations.Migration): | ||||
| 
 | ||||
|     dependencies = [ | ||||
|         ("authentik_audit", "0005_auto_20201005_2139"), | ||||
|         ("authentik_events", "0005_auto_20201005_2139"), | ||||
|     ] | ||||
| 
 | ||||
|     operations = [ | ||||
							
								
								
									
										41
									
								
								authentik/events/migrations/0007_auto_20201215_0939.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								authentik/events/migrations/0007_auto_20201215_0939.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| # Generated by Django 3.1.4 on 2020-12-15 09:39 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0006_auto_20201017_2024"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="event", | ||||
|             name="action", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("login", "Login"), | ||||
|                     ("login_failed", "Login Failed"), | ||||
|                     ("logout", "Logout"), | ||||
|                     ("user_write", "User Write"), | ||||
|                     ("suspicious_request", "Suspicious Request"), | ||||
|                     ("password_set", "Password Set"), | ||||
|                     ("token_view", "Token View"), | ||||
|                     ("invitation_created", "Invite Created"), | ||||
|                     ("invitation_used", "Invite Used"), | ||||
|                     ("authorize_application", "Authorize Application"), | ||||
|                     ("source_linked", "Source Linked"), | ||||
|                     ("impersonation_started", "Impersonation Started"), | ||||
|                     ("impersonation_ended", "Impersonation Ended"), | ||||
|                     ("policy_execution", "Policy Execution"), | ||||
|                     ("policy_exception", "Policy Exception"), | ||||
|                     ("property_mapping_exception", "Property Mapping Exception"), | ||||
|                     ("model_created", "Model Created"), | ||||
|                     ("model_updated", "Model Updated"), | ||||
|                     ("model_deleted", "Model Deleted"), | ||||
|                     ("custom_", "Custom Prefix"), | ||||
|                 ] | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										42
									
								
								authentik/events/migrations/0008_auto_20201220_1651.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								authentik/events/migrations/0008_auto_20201220_1651.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| # Generated by Django 3.1.4 on 2020-12-20 16:51 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0007_auto_20201215_0939"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="event", | ||||
|             name="action", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("login", "Login"), | ||||
|                     ("login_failed", "Login Failed"), | ||||
|                     ("logout", "Logout"), | ||||
|                     ("user_write", "User Write"), | ||||
|                     ("suspicious_request", "Suspicious Request"), | ||||
|                     ("password_set", "Password Set"), | ||||
|                     ("token_view", "Token View"), | ||||
|                     ("invitation_created", "Invite Created"), | ||||
|                     ("invitation_used", "Invite Used"), | ||||
|                     ("authorize_application", "Authorize Application"), | ||||
|                     ("source_linked", "Source Linked"), | ||||
|                     ("impersonation_started", "Impersonation Started"), | ||||
|                     ("impersonation_ended", "Impersonation Ended"), | ||||
|                     ("policy_execution", "Policy Execution"), | ||||
|                     ("policy_exception", "Policy Exception"), | ||||
|                     ("property_mapping_exception", "Property Mapping Exception"), | ||||
|                     ("model_created", "Model Created"), | ||||
|                     ("model_updated", "Model Updated"), | ||||
|                     ("model_deleted", "Model Deleted"), | ||||
|                     ("update_available", "Update Available"), | ||||
|                     ("custom_", "Custom Prefix"), | ||||
|                 ] | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
							
								
								
									
										42
									
								
								authentik/events/migrations/0009_auto_20201227_1210.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								authentik/events/migrations/0009_auto_20201227_1210.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| # Generated by Django 3.1.4 on 2020-12-27 12:10 | ||||
|  | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ("authentik_events", "0008_auto_20201220_1651"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.AlterField( | ||||
|             model_name="event", | ||||
|             name="action", | ||||
|             field=models.TextField( | ||||
|                 choices=[ | ||||
|                     ("login", "Login"), | ||||
|                     ("login_failed", "Login Failed"), | ||||
|                     ("logout", "Logout"), | ||||
|                     ("user_write", "User Write"), | ||||
|                     ("suspicious_request", "Suspicious Request"), | ||||
|                     ("password_set", "Password Set"), | ||||
|                     ("token_view", "Token View"), | ||||
|                     ("invitation_used", "Invite Used"), | ||||
|                     ("authorize_application", "Authorize Application"), | ||||
|                     ("source_linked", "Source Linked"), | ||||
|                     ("impersonation_started", "Impersonation Started"), | ||||
|                     ("impersonation_ended", "Impersonation Ended"), | ||||
|                     ("policy_execution", "Policy Execution"), | ||||
|                     ("policy_exception", "Policy Exception"), | ||||
|                     ("property_mapping_exception", "Property Mapping Exception"), | ||||
|                     ("configuration_error", "Configuration Error"), | ||||
|                     ("model_created", "Model Created"), | ||||
|                     ("model_updated", "Model Updated"), | ||||
|                     ("model_deleted", "Model Deleted"), | ||||
|                     ("update_available", "Update Available"), | ||||
|                     ("custom_", "Custom Prefix"), | ||||
|                 ] | ||||
|             ), | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,148 @@ | ||||
| # Generated by Django 3.1.4 on 2021-01-11 16:36 | ||||
|  | ||||
| import uuid | ||||
|  | ||||
| import django.db.models.deletion | ||||
| from django.conf import settings | ||||
| from django.db import migrations, models | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL), | ||||
|         ("authentik_policies", "0004_policy_execution_logging"), | ||||
|         ("authentik_core", "0016_auto_20201202_2234"), | ||||
|         ("authentik_events", "0009_auto_20201227_1210"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.CreateModel( | ||||
|             name="NotificationTransport", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "uuid", | ||||
|                     models.UUIDField( | ||||
|                         default=uuid.uuid4, | ||||
|                         editable=False, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.TextField(unique=True)), | ||||
|                 ( | ||||
|                     "mode", | ||||
|                     models.TextField( | ||||
|                         choices=[ | ||||
|                             ("webhook", "Generic Webhook"), | ||||
|                             ("webhook_slack", "Slack Webhook (Slack/Discord)"), | ||||
|                             ("email", "Email"), | ||||
|                         ] | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("webhook_url", models.TextField(blank=True)), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Notification Transport", | ||||
|                 "verbose_name_plural": "Notification Transports", | ||||
|             }, | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="NotificationRule", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "policybindingmodel_ptr", | ||||
|                     models.OneToOneField( | ||||
|                         auto_created=True, | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         parent_link=True, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                         to="authentik_policies.policybindingmodel", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("name", models.TextField(unique=True)), | ||||
|                 ( | ||||
|                     "severity", | ||||
|                     models.TextField( | ||||
|                         choices=[ | ||||
|                             ("notice", "Notice"), | ||||
|                             ("warning", "Warning"), | ||||
|                             ("alert", "Alert"), | ||||
|                         ], | ||||
|                         default="notice", | ||||
|                         help_text="Controls which severity level the created notifications will have.", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "group", | ||||
|                     models.ForeignKey( | ||||
|                         blank=True, | ||||
|                         help_text="Define which group of users this notification should be sent and shown to. If left empty, Notification won't ben sent.", | ||||
|                         null=True, | ||||
|                         on_delete=django.db.models.deletion.SET_NULL, | ||||
|                         to="authentik_core.group", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "transports", | ||||
|                     models.ManyToManyField( | ||||
|                         help_text="Select which transports should be used to notify the user. If none are selected, the notification will only be shown in the authentik UI.", | ||||
|                         to="authentik_events.NotificationTransport", | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Notification Rule", | ||||
|                 "verbose_name_plural": "Notification Rules", | ||||
|             }, | ||||
|             bases=("authentik_policies.policybindingmodel",), | ||||
|         ), | ||||
|         migrations.CreateModel( | ||||
|             name="Notification", | ||||
|             fields=[ | ||||
|                 ( | ||||
|                     "uuid", | ||||
|                     models.UUIDField( | ||||
|                         default=uuid.uuid4, | ||||
|                         editable=False, | ||||
|                         primary_key=True, | ||||
|                         serialize=False, | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "severity", | ||||
|                     models.TextField( | ||||
|                         choices=[ | ||||
|                             ("notice", "Notice"), | ||||
|                             ("warning", "Warning"), | ||||
|                             ("alert", "Alert"), | ||||
|                         ] | ||||
|                     ), | ||||
|                 ), | ||||
|                 ("body", models.TextField()), | ||||
|                 ("created", models.DateTimeField(auto_now_add=True)), | ||||
|                 ("seen", models.BooleanField(default=False)), | ||||
|                 ( | ||||
|                     "event", | ||||
|                     models.ForeignKey( | ||||
|                         blank=True, | ||||
|                         null=True, | ||||
|                         on_delete=django.db.models.deletion.SET_NULL, | ||||
|                         to="authentik_events.event", | ||||
|                     ), | ||||
|                 ), | ||||
|                 ( | ||||
|                     "user", | ||||
|                     models.ForeignKey( | ||||
|                         on_delete=django.db.models.deletion.CASCADE, | ||||
|                         to=settings.AUTH_USER_MODEL, | ||||
|                     ), | ||||
|                 ), | ||||
|             ], | ||||
|             options={ | ||||
|                 "verbose_name": "Notification", | ||||
|                 "verbose_name_plural": "Notifications", | ||||
|             }, | ||||
|         ), | ||||
|     ] | ||||
| @ -0,0 +1,165 @@ | ||||
| # Generated by Django 3.1.4 on 2021-01-10 18:57 | ||||
|  | ||||
| from django.apps.registry import Apps | ||||
| from django.db import migrations | ||||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||||
|  | ||||
| from authentik.events.models import EventAction, NotificationSeverity, TransportMode | ||||
|  | ||||
|  | ||||
| def notify_configuration_error(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     Group = apps.get_model("authentik_core", "Group") | ||||
|     PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding") | ||||
|     EventMatcherPolicy = apps.get_model( | ||||
|         "authentik_policies_event_matcher", "EventMatcherPolicy" | ||||
|     ) | ||||
|     NotificationRule = apps.get_model("authentik_events", "NotificationRule") | ||||
|     NotificationTransport = apps.get_model("authentik_events", "NotificationTransport") | ||||
|  | ||||
|     admin_group = ( | ||||
|         Group.objects.using(db_alias) | ||||
|         .filter(name="authentik Admins", is_superuser=True) | ||||
|         .first() | ||||
|     ) | ||||
|  | ||||
|     policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create( | ||||
|         name="default-match-configuration-error", | ||||
|         defaults={"action": EventAction.CONFIGURATION_ERROR}, | ||||
|     ) | ||||
|     trigger, _ = NotificationRule.objects.using(db_alias).update_or_create( | ||||
|         name="default-notify-configuration-error", | ||||
|         defaults={"group": admin_group, "severity": NotificationSeverity.ALERT}, | ||||
|     ) | ||||
|     trigger.transports.set( | ||||
|         NotificationTransport.objects.using(db_alias).filter( | ||||
|             name="default-email-transport" | ||||
|         ) | ||||
|     ) | ||||
|     trigger.save() | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         target=trigger, | ||||
|         policy=policy, | ||||
|         defaults={ | ||||
|             "order": 0, | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def notify_update(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     Group = apps.get_model("authentik_core", "Group") | ||||
|     PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding") | ||||
|     EventMatcherPolicy = apps.get_model( | ||||
|         "authentik_policies_event_matcher", "EventMatcherPolicy" | ||||
|     ) | ||||
|     NotificationRule = apps.get_model("authentik_events", "NotificationRule") | ||||
|     NotificationTransport = apps.get_model("authentik_events", "NotificationTransport") | ||||
|  | ||||
|     admin_group = ( | ||||
|         Group.objects.using(db_alias) | ||||
|         .filter(name="authentik Admins", is_superuser=True) | ||||
|         .first() | ||||
|     ) | ||||
|  | ||||
|     policy, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create( | ||||
|         name="default-match-update", | ||||
|         defaults={"action": EventAction.UPDATE_AVAILABLE}, | ||||
|     ) | ||||
|     trigger, _ = NotificationRule.objects.using(db_alias).update_or_create( | ||||
|         name="default-notify-update", | ||||
|         defaults={"group": admin_group, "severity": NotificationSeverity.ALERT}, | ||||
|     ) | ||||
|     trigger.transports.set( | ||||
|         NotificationTransport.objects.using(db_alias).filter( | ||||
|             name="default-email-transport" | ||||
|         ) | ||||
|     ) | ||||
|     trigger.save() | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         target=trigger, | ||||
|         policy=policy, | ||||
|         defaults={ | ||||
|             "order": 0, | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def notify_exception(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     Group = apps.get_model("authentik_core", "Group") | ||||
|     PolicyBinding = apps.get_model("authentik_policies", "PolicyBinding") | ||||
|     EventMatcherPolicy = apps.get_model( | ||||
|         "authentik_policies_event_matcher", "EventMatcherPolicy" | ||||
|     ) | ||||
|     NotificationRule = apps.get_model("authentik_events", "NotificationRule") | ||||
|     NotificationTransport = apps.get_model("authentik_events", "NotificationTransport") | ||||
|  | ||||
|     admin_group = ( | ||||
|         Group.objects.using(db_alias) | ||||
|         .filter(name="authentik Admins", is_superuser=True) | ||||
|         .first() | ||||
|     ) | ||||
|  | ||||
|     policy_policy_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create( | ||||
|         name="default-match-policy-exception", | ||||
|         defaults={"action": EventAction.POLICY_EXCEPTION}, | ||||
|     ) | ||||
|     policy_pm_exc, _ = EventMatcherPolicy.objects.using(db_alias).update_or_create( | ||||
|         name="default-match-property-mapping-exception", | ||||
|         defaults={"action": EventAction.PROPERTY_MAPPING_EXCEPTION}, | ||||
|     ) | ||||
|     trigger, _ = NotificationRule.objects.using(db_alias).update_or_create( | ||||
|         name="default-notify-exception", | ||||
|         defaults={"group": admin_group, "severity": NotificationSeverity.ALERT}, | ||||
|     ) | ||||
|     trigger.transports.set( | ||||
|         NotificationTransport.objects.using(db_alias).filter( | ||||
|             name="default-email-transport" | ||||
|         ) | ||||
|     ) | ||||
|     trigger.save() | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         target=trigger, | ||||
|         policy=policy_policy_exc, | ||||
|         defaults={ | ||||
|             "order": 0, | ||||
|         }, | ||||
|     ) | ||||
|     PolicyBinding.objects.using(db_alias).update_or_create( | ||||
|         target=trigger, | ||||
|         policy=policy_pm_exc, | ||||
|         defaults={ | ||||
|             "order": 1, | ||||
|         }, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| def transport_email_global(apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||||
|     db_alias = schema_editor.connection.alias | ||||
|     NotificationTransport = apps.get_model("authentik_events", "NotificationTransport") | ||||
|  | ||||
|     NotificationTransport.objects.using(db_alias).update_or_create( | ||||
|         name="default-email-transport", | ||||
|         defaults={"mode": TransportMode.EMAIL}, | ||||
|     ) | ||||
|  | ||||
|  | ||||
| class Migration(migrations.Migration): | ||||
|  | ||||
|     dependencies = [ | ||||
|         ( | ||||
|             "authentik_events", | ||||
|             "0010_notification_notificationtransport_notificationrule", | ||||
|         ), | ||||
|         ("authentik_core", "0016_auto_20201202_2234"), | ||||
|         ("authentik_policies_event_matcher", "0003_auto_20210110_1907"), | ||||
|         ("authentik_policies", "0004_policy_execution_logging"), | ||||
|     ] | ||||
|  | ||||
|     operations = [ | ||||
|         migrations.RunPython(transport_email_global), | ||||
|         migrations.RunPython(notify_configuration_error), | ||||
|         migrations.RunPython(notify_update), | ||||
|         migrations.RunPython(notify_exception), | ||||
|     ] | ||||
							
								
								
									
										365
									
								
								authentik/events/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										365
									
								
								authentik/events/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,365 @@ | ||||
| """authentik events models""" | ||||
| from inspect import getmodule, stack | ||||
| from smtplib import SMTPException | ||||
| from typing import Optional, Union | ||||
| from uuid import uuid4 | ||||
|  | ||||
| from django.conf import settings | ||||
| from django.core.exceptions import ValidationError | ||||
| from django.db import models | ||||
| from django.http import HttpRequest | ||||
| from django.utils.translation import gettext as _ | ||||
| from requests import RequestException, post | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import __version__ | ||||
| from authentik.core.middleware import ( | ||||
|     SESSION_IMPERSONATE_ORIGINAL_USER, | ||||
|     SESSION_IMPERSONATE_USER, | ||||
| ) | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.events.utils import cleanse_dict, get_user, sanitize_dict | ||||
| from authentik.lib.sentry import SentryIgnoredException | ||||
| from authentik.lib.utils.http import get_client_ip | ||||
| from authentik.policies.models import PolicyBindingModel | ||||
| from authentik.stages.email.tasks import send_mail | ||||
| from authentik.stages.email.utils import TemplateEmailMessage | ||||
|  | ||||
| LOGGER = get_logger("authentik.events") | ||||
|  | ||||
|  | ||||
| class NotificationTransportError(SentryIgnoredException): | ||||
|     """Error raised when a notification fails to be delivered""" | ||||
|  | ||||
|  | ||||
| class EventAction(models.TextChoices): | ||||
|     """All possible actions to save into the events log""" | ||||
|  | ||||
|     LOGIN = "login" | ||||
|     LOGIN_FAILED = "login_failed" | ||||
|     LOGOUT = "logout" | ||||
|  | ||||
|     USER_WRITE = "user_write" | ||||
|     SUSPICIOUS_REQUEST = "suspicious_request" | ||||
|     PASSWORD_SET = "password_set"  # noqa # nosec | ||||
|  | ||||
|     TOKEN_VIEW = "token_view"  # nosec | ||||
|  | ||||
|     INVITE_USED = "invitation_used" | ||||
|  | ||||
|     AUTHORIZE_APPLICATION = "authorize_application" | ||||
|     SOURCE_LINKED = "source_linked" | ||||
|  | ||||
|     IMPERSONATION_STARTED = "impersonation_started" | ||||
|     IMPERSONATION_ENDED = "impersonation_ended" | ||||
|  | ||||
|     POLICY_EXECUTION = "policy_execution" | ||||
|     POLICY_EXCEPTION = "policy_exception" | ||||
|     PROPERTY_MAPPING_EXCEPTION = "property_mapping_exception" | ||||
|  | ||||
|     CONFIGURATION_ERROR = "configuration_error" | ||||
|  | ||||
|     MODEL_CREATED = "model_created" | ||||
|     MODEL_UPDATED = "model_updated" | ||||
|     MODEL_DELETED = "model_deleted" | ||||
|  | ||||
|     UPDATE_AVAILABLE = "update_available" | ||||
|  | ||||
|     CUSTOM_PREFIX = "custom_" | ||||
|  | ||||
|  | ||||
| class Event(models.Model): | ||||
|     """An individual Audit/Metrics/Notification/Error Event""" | ||||
|  | ||||
|     event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|     user = models.JSONField(default=dict) | ||||
|     action = models.TextField(choices=EventAction.choices) | ||||
|     app = models.TextField() | ||||
|     context = models.JSONField(default=dict, blank=True) | ||||
|     client_ip = models.GenericIPAddressField(null=True) | ||||
|     created = models.DateTimeField(auto_now_add=True) | ||||
|  | ||||
|     @staticmethod | ||||
|     def _get_app_from_request(request: HttpRequest) -> str: | ||||
|         if not isinstance(request, HttpRequest): | ||||
|             return "" | ||||
|         return request.resolver_match.app_name | ||||
|  | ||||
|     @staticmethod | ||||
|     def new( | ||||
|         action: Union[str, EventAction], | ||||
|         app: Optional[str] = None, | ||||
|         _inspect_offset: int = 1, | ||||
|         **kwargs, | ||||
|     ) -> "Event": | ||||
|         """Create new Event instance from arguments. Instance is NOT saved.""" | ||||
|         if not isinstance(action, EventAction): | ||||
|             action = EventAction.CUSTOM_PREFIX + action | ||||
|         if not app: | ||||
|             app = getmodule(stack()[_inspect_offset][0]).__name__ | ||||
|         cleaned_kwargs = cleanse_dict(sanitize_dict(kwargs)) | ||||
|         event = Event(action=action, app=app, context=cleaned_kwargs) | ||||
|         return event | ||||
|  | ||||
|     def set_user(self, user: User) -> "Event": | ||||
|         """Set `.user` based on user, ensuring the correct attributes are copied. | ||||
|         This should only be used when self.from_http is *not* used.""" | ||||
|         self.user = get_user(user) | ||||
|         return self | ||||
|  | ||||
|     def from_http( | ||||
|         self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None | ||||
|     ) -> "Event": | ||||
|         """Add data from a Django-HttpRequest, allowing the creation of | ||||
|         Events independently from requests. | ||||
|         `user` arguments optionally overrides user from requests.""" | ||||
|         if hasattr(request, "user"): | ||||
|             original_user = None | ||||
|             if hasattr(request, "session"): | ||||
|                 original_user = request.session.get( | ||||
|                     SESSION_IMPERSONATE_ORIGINAL_USER, None | ||||
|                 ) | ||||
|             self.user = get_user(request.user, original_user) | ||||
|         if user: | ||||
|             self.user = get_user(user) | ||||
|         # Check if we're currently impersonating, and add that user | ||||
|         if hasattr(request, "session"): | ||||
|             if SESSION_IMPERSONATE_ORIGINAL_USER in request.session: | ||||
|                 self.user = get_user(request.session[SESSION_IMPERSONATE_ORIGINAL_USER]) | ||||
|                 self.user["on_behalf_of"] = get_user( | ||||
|                     request.session[SESSION_IMPERSONATE_USER] | ||||
|                 ) | ||||
|         # User 255.255.255.255 as fallback if IP cannot be determined | ||||
|         self.client_ip = get_client_ip(request) or "255.255.255.255" | ||||
|         # If there's no app set, we get it from the requests too | ||||
|         if not self.app: | ||||
|             self.app = Event._get_app_from_request(request) | ||||
|         self.save() | ||||
|         return self | ||||
|  | ||||
|     def save(self, *args, **kwargs): | ||||
|         if not self._state.adding: | ||||
|             raise ValidationError("you may not edit an existing Event") | ||||
|         LOGGER.debug( | ||||
|             "Created Event", | ||||
|             action=self.action, | ||||
|             context=self.context, | ||||
|             client_ip=self.client_ip, | ||||
|             user=self.user, | ||||
|         ) | ||||
|         return super().save(*args, **kwargs) | ||||
|  | ||||
|     @property | ||||
|     def summary(self) -> str: | ||||
|         """Return a summary of this event.""" | ||||
|         if "message" in self.context: | ||||
|             return self.context["message"] | ||||
|         return f"{self.action}: {self.context}" | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"<Event action={self.action} user={self.user} context={self.context}>" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Event") | ||||
|         verbose_name_plural = _("Events") | ||||
|  | ||||
|  | ||||
| class TransportMode(models.TextChoices): | ||||
|     """Modes that a notification transport can send a notification""" | ||||
|  | ||||
|     WEBHOOK = "webhook", _("Generic Webhook") | ||||
|     WEBHOOK_SLACK = "webhook_slack", _("Slack Webhook (Slack/Discord)") | ||||
|     EMAIL = "email", _("Email") | ||||
|  | ||||
|  | ||||
| class NotificationTransport(models.Model): | ||||
|     """Action which is executed when a Rule matches""" | ||||
|  | ||||
|     uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|  | ||||
|     name = models.TextField(unique=True) | ||||
|     mode = models.TextField(choices=TransportMode.choices) | ||||
|  | ||||
|     webhook_url = models.TextField(blank=True) | ||||
|  | ||||
|     def send(self, notification: "Notification") -> list[str]: | ||||
|         """Send notification to user, called from async task""" | ||||
|         if self.mode == TransportMode.WEBHOOK: | ||||
|             return self.send_webhook(notification) | ||||
|         if self.mode == TransportMode.WEBHOOK_SLACK: | ||||
|             return self.send_webhook_slack(notification) | ||||
|         if self.mode == TransportMode.EMAIL: | ||||
|             return self.send_email(notification) | ||||
|         raise ValueError(f"Invalid mode {self.mode} set") | ||||
|  | ||||
|     def send_webhook(self, notification: "Notification") -> list[str]: | ||||
|         """Send notification to generic webhook""" | ||||
|         try: | ||||
|             response = post( | ||||
|                 self.webhook_url, | ||||
|                 json={ | ||||
|                     "body": notification.body, | ||||
|                     "severity": notification.severity, | ||||
|                     "user_email": notification.user.email, | ||||
|                     "user_username": notification.user.username, | ||||
|                 }, | ||||
|             ) | ||||
|             response.raise_for_status() | ||||
|         except RequestException as exc: | ||||
|             raise NotificationTransportError(exc.response.text) from exc | ||||
|         return [ | ||||
|             response.status_code, | ||||
|             response.text, | ||||
|         ] | ||||
|  | ||||
|     def send_webhook_slack(self, notification: "Notification") -> list[str]: | ||||
|         """Send notification to slack or slack-compatible endpoints""" | ||||
|         fields = [ | ||||
|             { | ||||
|                 "title": _("Severity"), | ||||
|                 "value": notification.severity, | ||||
|                 "short": True, | ||||
|             }, | ||||
|             { | ||||
|                 "title": _("Dispatched for user"), | ||||
|                 "value": str(notification.user), | ||||
|                 "short": True, | ||||
|             }, | ||||
|         ] | ||||
|         if notification.event: | ||||
|             for key, value in notification.event.context.items(): | ||||
|                 if not isinstance(value, str): | ||||
|                     continue | ||||
|                 # https://birdie0.github.io/discord-webhooks-guide/other/field_limits.html | ||||
|                 if len(fields) >= 25: | ||||
|                     continue | ||||
|                 fields.append({"title": key[:256], "value": value[:1024]}) | ||||
|         body = { | ||||
|             "username": "authentik", | ||||
|             "icon_url": "https://goauthentik.io/img/icon.png", | ||||
|             "attachments": [ | ||||
|                 { | ||||
|                     "author_name": "authentik", | ||||
|                     "author_link": "https://goauthentik.io", | ||||
|                     "author_icon": "https://goauthentik.io/img/icon.png", | ||||
|                     "title": notification.body, | ||||
|                     "color": "#fd4b2d", | ||||
|                     "fields": fields, | ||||
|                     "footer": f"authentik v{__version__}", | ||||
|                 } | ||||
|             ], | ||||
|         } | ||||
|         if notification.event: | ||||
|             body["attachments"][0]["title"] = notification.event.action | ||||
|             body["attachments"][0]["text"] = notification.event.action | ||||
|         try: | ||||
|             response = post(self.webhook_url, json=body) | ||||
|             response.raise_for_status() | ||||
|         except RequestException as exc: | ||||
|             raise NotificationTransportError(exc.response.text) from exc | ||||
|         return [ | ||||
|             response.status_code, | ||||
|             response.text, | ||||
|         ] | ||||
|  | ||||
|     def send_email(self, notification: "Notification") -> list[str]: | ||||
|         """Send notification via global email configuration""" | ||||
|         body_trunc = ( | ||||
|             (notification.body[:75] + "..") | ||||
|             if len(notification.body) > 75 | ||||
|             else notification.body | ||||
|         ) | ||||
|         mail = TemplateEmailMessage( | ||||
|             subject=f"authentik Notification: {body_trunc}", | ||||
|             template_name="email/setup.html", | ||||
|             to=[notification.user.email], | ||||
|             template_context={ | ||||
|                 "body": notification.body, | ||||
|             }, | ||||
|         ) | ||||
|         # Email is sent directly here, as the call to send() should have been from a task. | ||||
|         try: | ||||
|             # pyright: reportGeneralTypeIssues=false | ||||
|             return send_mail(mail.__dict__)  # pylint: disable=no-value-for-parameter | ||||
|         except (SMTPException, ConnectionError) as exc: | ||||
|             raise NotificationTransportError from exc | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"Notification Transport {self.name}" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Notification Transport") | ||||
|         verbose_name_plural = _("Notification Transports") | ||||
|  | ||||
|  | ||||
| class NotificationSeverity(models.TextChoices): | ||||
|     """Severity images that a notification can have""" | ||||
|  | ||||
|     NOTICE = "notice", _("Notice") | ||||
|     WARNING = "warning", _("Warning") | ||||
|     ALERT = "alert", _("Alert") | ||||
|  | ||||
|  | ||||
| class Notification(models.Model): | ||||
|     """Event Notification""" | ||||
|  | ||||
|     uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) | ||||
|     severity = models.TextField(choices=NotificationSeverity.choices) | ||||
|     body = models.TextField() | ||||
|     created = models.DateTimeField(auto_now_add=True) | ||||
|     event = models.ForeignKey(Event, on_delete=models.SET_NULL, null=True, blank=True) | ||||
|     seen = models.BooleanField(default=False) | ||||
|     user = models.ForeignKey(User, on_delete=models.CASCADE) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         body_trunc = (self.body[:75] + "..") if len(self.body) > 75 else self.body | ||||
|         return f"Notification for user {self.user}: {body_trunc}" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Notification") | ||||
|         verbose_name_plural = _("Notifications") | ||||
|  | ||||
|  | ||||
| class NotificationRule(PolicyBindingModel): | ||||
|     """Decide when to create a Notification based on policies attached to this object.""" | ||||
|  | ||||
|     name = models.TextField(unique=True) | ||||
|     transports = models.ManyToManyField( | ||||
|         NotificationTransport, | ||||
|         help_text=_( | ||||
|             ( | ||||
|                 "Select which transports should be used to notify the user. If none are " | ||||
|                 "selected, the notification will only be shown in the authentik UI." | ||||
|             ) | ||||
|         ), | ||||
|     ) | ||||
|     severity = models.TextField( | ||||
|         choices=NotificationSeverity.choices, | ||||
|         default=NotificationSeverity.NOTICE, | ||||
|         help_text=_( | ||||
|             "Controls which severity level the created notifications will have." | ||||
|         ), | ||||
|     ) | ||||
|     group = models.ForeignKey( | ||||
|         Group, | ||||
|         help_text=_( | ||||
|             ( | ||||
|                 "Define which group of users this notification should be sent and shown to. " | ||||
|                 "If left empty, Notification won't ben sent." | ||||
|             ) | ||||
|         ), | ||||
|         null=True, | ||||
|         blank=True, | ||||
|         on_delete=models.SET_NULL, | ||||
|     ) | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"Notification Rule {self.name}" | ||||
|  | ||||
|     class Meta: | ||||
|  | ||||
|         verbose_name = _("Notification Rule") | ||||
|         verbose_name_plural = _("Notification Rules") | ||||
| @ -1,4 +1,4 @@ | ||||
| """authentik audit signal listener""" | ||||
| """authentik events signal listener""" | ||||
| from threading import Thread | ||||
| from typing import Any, Dict, Optional | ||||
| 
 | ||||
| @ -7,14 +7,18 @@ from django.contrib.auth.signals import ( | ||||
|     user_logged_out, | ||||
|     user_login_failed, | ||||
| ) | ||||
| from django.db.models.signals import post_save | ||||
| from django.dispatch import receiver | ||||
| from django.http import HttpRequest | ||||
| 
 | ||||
| from authentik.audit.models import Event, EventAction | ||||
| from authentik.core.models import User | ||||
| from authentik.core.signals import password_changed | ||||
| from authentik.events.models import Event, EventAction | ||||
| from authentik.events.tasks import event_notification_handler | ||||
| from authentik.flows.planner import PLAN_CONTEXT_SOURCE, FlowPlan | ||||
| from authentik.flows.views import SESSION_KEY_PLAN | ||||
| from authentik.stages.invitation.models import Invitation | ||||
| from authentik.stages.invitation.signals import invitation_created, invitation_used | ||||
| from authentik.stages.invitation.signals import invitation_used | ||||
| from authentik.stages.user_write.signals import user_write | ||||
| 
 | ||||
| 
 | ||||
| @ -44,6 +48,11 @@ class EventNewThread(Thread): | ||||
| def on_user_logged_in(sender, request: HttpRequest, user: User, **_): | ||||
|     """Log successful login""" | ||||
|     thread = EventNewThread(EventAction.LOGIN, request) | ||||
|     if SESSION_KEY_PLAN in request.session: | ||||
|         flow_plan: FlowPlan = request.session[SESSION_KEY_PLAN] | ||||
|         if PLAN_CONTEXT_SOURCE in flow_plan.context: | ||||
|             # Login request came from an external source, save it in the context | ||||
|             thread.kwargs["using_source"] = flow_plan.context[PLAN_CONTEXT_SOURCE] | ||||
|     thread.user = user | ||||
|     thread.run() | ||||
| 
 | ||||
| @ -79,16 +88,6 @@ def on_user_login_failed( | ||||
|     thread.run() | ||||
| 
 | ||||
| 
 | ||||
| @receiver(invitation_created) | ||||
| # pylint: disable=unused-argument | ||||
| def on_invitation_created(sender, request: HttpRequest, invitation: Invitation, **_): | ||||
|     """Log Invitation creation""" | ||||
|     thread = EventNewThread( | ||||
|         EventAction.INVITE_CREATED, request, invitation_uuid=invitation.invite_uuid.hex | ||||
|     ) | ||||
|     thread.run() | ||||
| 
 | ||||
| 
 | ||||
| @receiver(invitation_used) | ||||
| # pylint: disable=unused-argument | ||||
| def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_): | ||||
| @ -105,3 +104,10 @@ def on_password_changed(sender, user: User, password: str, **_): | ||||
|     """Log password change""" | ||||
|     thread = EventNewThread(EventAction.PASSWORD_SET, None, user=user) | ||||
|     thread.run() | ||||
| 
 | ||||
| 
 | ||||
| @receiver(post_save, sender=Event) | ||||
| # pylint: disable=unused-argument | ||||
| def event_post_save_notification(sender, instance: Event, **_): | ||||
|     """Start task to check if any policies trigger an notification on this event""" | ||||
|     event_notification_handler.delay(instance.event_uuid.hex) | ||||
							
								
								
									
										99
									
								
								authentik/events/tasks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								authentik/events/tasks.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,99 @@ | ||||
| """Event notification tasks""" | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| from structlog import get_logger | ||||
|  | ||||
| from authentik.events.models import ( | ||||
|     Event, | ||||
|     Notification, | ||||
|     NotificationRule, | ||||
|     NotificationTransport, | ||||
|     NotificationTransportError, | ||||
| ) | ||||
| from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus | ||||
| from authentik.policies.engine import PolicyEngine, PolicyEngineMode | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.root.celery import CELERY_APP | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def event_notification_handler(event_uuid: str): | ||||
|     """Start task for each trigger definition""" | ||||
|     for trigger in NotificationRule.objects.all(): | ||||
|         event_trigger_handler.apply_async( | ||||
|             args=[event_uuid, trigger.name], queue="authentik_events" | ||||
|         ) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task() | ||||
| def event_trigger_handler(event_uuid: str, trigger_name: str): | ||||
|     """Check if policies attached to NotificationRule match event""" | ||||
|     event: Event = Event.objects.get(event_uuid=event_uuid) | ||||
|     trigger: NotificationRule = NotificationRule.objects.get(name=trigger_name) | ||||
|  | ||||
|     if "policy_uuid" in event.context: | ||||
|         policy_uuid = event.context["policy_uuid"] | ||||
|         if PolicyBinding.objects.filter( | ||||
|             target__in=NotificationRule.objects.all().values_list( | ||||
|                 "pbm_uuid", flat=True | ||||
|             ), | ||||
|             policy=policy_uuid, | ||||
|         ).exists(): | ||||
|             # If policy that caused this event to be created is attached | ||||
|             # to *any* NotificationRule, we return early. | ||||
|             # This is the most effective way to prevent infinite loops. | ||||
|             LOGGER.debug( | ||||
|                 "e(trigger): attempting to prevent infinite loop", trigger=trigger | ||||
|             ) | ||||
|             return | ||||
|  | ||||
|     if not trigger.group: | ||||
|         LOGGER.debug("e(trigger): trigger has no group", trigger=trigger) | ||||
|         return | ||||
|  | ||||
|     LOGGER.debug("e(trigger): checking if trigger applies", trigger=trigger) | ||||
|     policy_engine = PolicyEngine(trigger, get_anonymous_user()) | ||||
|     policy_engine.mode = PolicyEngineMode.MODE_OR | ||||
|     policy_engine.empty_result = False | ||||
|     policy_engine.use_cache = False | ||||
|     policy_engine.request.context["event"] = event | ||||
|     policy_engine.build() | ||||
|     result = policy_engine.result | ||||
|     if not result.passing: | ||||
|         return | ||||
|  | ||||
|     LOGGER.debug("e(trigger): event trigger matched", trigger=trigger) | ||||
|     # Create the notification objects | ||||
|     for user in trigger.group.users.all(): | ||||
|         notification = Notification.objects.create( | ||||
|             severity=trigger.severity, body=event.summary, event=event, user=user | ||||
|         ) | ||||
|  | ||||
|         for transport in trigger.transports.all(): | ||||
|             notification_transport.apply_async( | ||||
|                 args=[notification.pk, transport.pk], queue="authentik_events" | ||||
|             ) | ||||
|  | ||||
|  | ||||
| @CELERY_APP.task( | ||||
|     bind=True, | ||||
|     autoretry_for=(NotificationTransportError,), | ||||
|     retry_backoff=True, | ||||
|     base=MonitoredTask, | ||||
| ) | ||||
| def notification_transport( | ||||
|     self: MonitoredTask, notification_pk: int, transport_pk: int | ||||
| ): | ||||
|     """Send notification over specified transport""" | ||||
|     self.save_on_success = False | ||||
|     try: | ||||
|         notification: Notification = Notification.objects.get(pk=notification_pk) | ||||
|         transport: NotificationTransport = NotificationTransport.objects.get( | ||||
|             pk=transport_pk | ||||
|         ) | ||||
|         transport.send(notification) | ||||
|         self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL)) | ||||
|     except NotificationTransportError as exc: | ||||
|         self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc)) | ||||
|         raise exc | ||||
							
								
								
									
										0
									
								
								authentik/events/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/events/tests/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								authentik/events/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								authentik/events/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| """Event API tests""" | ||||
|  | ||||
| from django.shortcuts import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| class TestEventsAPI(APITestCase): | ||||
|     """Test Event API""" | ||||
|  | ||||
|     def test_top_n(self): | ||||
|         """Test top_per_user""" | ||||
|         user = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(user) | ||||
|  | ||||
|         event = Event.new(EventAction.AUTHORIZE_APPLICATION) | ||||
|         event.save()  # We save to ensure nothing is un-saveable | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:event-top-per-user"), | ||||
|             data={"filter_action": EventAction.AUTHORIZE_APPLICATION}, | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
| @ -1,26 +1,37 @@ | ||||
| """audit event tests""" | ||||
| """event tests""" | ||||
| 
 | ||||
| from django.contrib.contenttypes.models import ContentType | ||||
| from django.test import TestCase | ||||
| from guardian.shortcuts import get_anonymous_user | ||||
| 
 | ||||
| from authentik.audit.models import Event | ||||
| from authentik.core.models import Group | ||||
| from authentik.events.models import Event | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| 
 | ||||
| 
 | ||||
| class TestAuditEvent(TestCase): | ||||
|     """Test Audit Event""" | ||||
| class TestEvents(TestCase): | ||||
|     """Test Event""" | ||||
| 
 | ||||
|     def test_new_with_model(self): | ||||
|         """Create a new Event passing a model as kwarg""" | ||||
|         event = Event.new("unittest", test={"model": get_anonymous_user()}) | ||||
|         test_model = Group.objects.create(name="test") | ||||
|         event = Event.new("unittest", test={"model": test_model}) | ||||
|         event.save()  # We save to ensure nothing is un-saveable | ||||
|         model_content_type = ContentType.objects.get_for_model(get_anonymous_user()) | ||||
|         model_content_type = ContentType.objects.get_for_model(test_model) | ||||
|         self.assertEqual( | ||||
|             event.context.get("test").get("model").get("app"), | ||||
|             model_content_type.app_label, | ||||
|         ) | ||||
| 
 | ||||
|     def test_new_with_user(self): | ||||
|         """Create a new Event passing a user as kwarg""" | ||||
|         event = Event.new("unittest", test={"model": get_anonymous_user()}) | ||||
|         event.save()  # We save to ensure nothing is un-saveable | ||||
|         self.assertEqual( | ||||
|             event.context.get("test").get("model").get("username"), | ||||
|             get_anonymous_user().username, | ||||
|         ) | ||||
| 
 | ||||
|     def test_new_with_uuid_model(self): | ||||
|         """Create a new Event passing a model (with UUID PK) as kwarg""" | ||||
|         temp_model = DummyPolicy.objects.create(name="test", result=True) | ||||
							
								
								
									
										48
									
								
								authentik/events/tests/test_middleware.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								authentik/events/tests/test_middleware.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| """Event Middleware tests""" | ||||
|  | ||||
| from django.shortcuts import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import Application, User | ||||
| from authentik.events.models import Event, EventAction | ||||
|  | ||||
|  | ||||
| class TestEventsMiddleware(APITestCase): | ||||
|     """Test Event Middleware""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         super().setUp() | ||||
|         self.user = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(self.user) | ||||
|  | ||||
|     def test_create(self): | ||||
|         """Test model creation event""" | ||||
|         self.client.post( | ||||
|             reverse("authentik_api:application-list"), | ||||
|             data={"name": "test-create", "slug": "test-create"}, | ||||
|         ) | ||||
|         self.assertTrue(Application.objects.filter(name="test-create").exists()) | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.MODEL_CREATED, | ||||
|                 context__model__model_name="application", | ||||
|                 context__model__app="authentik_core", | ||||
|                 context__model__name="test-create", | ||||
|             ).exists() | ||||
|         ) | ||||
|  | ||||
|     def test_delete(self): | ||||
|         """Test model creation event""" | ||||
|         Application.objects.create(name="test-delete", slug="test-delete") | ||||
|         self.client.delete( | ||||
|             reverse("authentik_api:application-detail", kwargs={"slug": "test-delete"}) | ||||
|         ) | ||||
|         self.assertFalse(Application.objects.filter(name="test").exists()) | ||||
|         self.assertTrue( | ||||
|             Event.objects.filter( | ||||
|                 action=EventAction.MODEL_DELETED, | ||||
|                 context__model__model_name="application", | ||||
|                 context__model__app="authentik_core", | ||||
|                 context__model__name="test-delete", | ||||
|             ).exists() | ||||
|         ) | ||||
							
								
								
									
										90
									
								
								authentik/events/tests/test_notifications.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								authentik/events/tests/test_notifications.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,90 @@ | ||||
| """Notification tests""" | ||||
|  | ||||
| from unittest.mock import MagicMock, patch | ||||
|  | ||||
| from django.test import TestCase | ||||
|  | ||||
| from authentik.core.models import Group, User | ||||
| from authentik.events.models import ( | ||||
|     Event, | ||||
|     EventAction, | ||||
|     NotificationRule, | ||||
|     NotificationTransport, | ||||
| ) | ||||
| from authentik.policies.event_matcher.models import EventMatcherPolicy | ||||
| from authentik.policies.exceptions import PolicyException | ||||
| from authentik.policies.models import PolicyBinding | ||||
|  | ||||
|  | ||||
| class TestEventsNotifications(TestCase): | ||||
|     """Test Event Notifications""" | ||||
|  | ||||
|     def setUp(self) -> None: | ||||
|         self.group = Group.objects.create(name="test-group") | ||||
|         self.user = User.objects.create(name="test-user") | ||||
|         self.group.users.add(self.user) | ||||
|         self.group.save() | ||||
|  | ||||
|     def test_trigger_empty(self): | ||||
|         """Test trigger without any policies attached""" | ||||
|         transport = NotificationTransport.objects.create(name="transport") | ||||
|         trigger = NotificationRule.objects.create(name="trigger", group=self.group) | ||||
|         trigger.transports.add(transport) | ||||
|         trigger.save() | ||||
|  | ||||
|         execute_mock = MagicMock() | ||||
|         with patch("authentik.events.models.NotificationTransport.send", execute_mock): | ||||
|             Event.new(EventAction.CUSTOM_PREFIX).save() | ||||
|         self.assertEqual(execute_mock.call_count, 0) | ||||
|  | ||||
|     def test_trigger_single(self): | ||||
|         """Test simple transport triggering""" | ||||
|         transport = NotificationTransport.objects.create(name="transport") | ||||
|         trigger = NotificationRule.objects.create(name="trigger", group=self.group) | ||||
|         trigger.transports.add(transport) | ||||
|         trigger.save() | ||||
|         matcher = EventMatcherPolicy.objects.create( | ||||
|             name="matcher", action=EventAction.CUSTOM_PREFIX | ||||
|         ) | ||||
|         PolicyBinding.objects.create(target=trigger, policy=matcher, order=0) | ||||
|  | ||||
|         execute_mock = MagicMock() | ||||
|         with patch("authentik.events.models.NotificationTransport.send", execute_mock): | ||||
|             Event.new(EventAction.CUSTOM_PREFIX).save() | ||||
|         self.assertEqual(execute_mock.call_count, 1) | ||||
|  | ||||
|     def test_trigger_no_group(self): | ||||
|         """Test trigger without group""" | ||||
|         trigger = NotificationRule.objects.create(name="trigger") | ||||
|         matcher = EventMatcherPolicy.objects.create( | ||||
|             name="matcher", action=EventAction.CUSTOM_PREFIX | ||||
|         ) | ||||
|         PolicyBinding.objects.create(target=trigger, policy=matcher, order=0) | ||||
|  | ||||
|         execute_mock = MagicMock() | ||||
|         with patch("authentik.events.models.NotificationTransport.send", execute_mock): | ||||
|             Event.new(EventAction.CUSTOM_PREFIX).save() | ||||
|         self.assertEqual(execute_mock.call_count, 0) | ||||
|  | ||||
|     def test_policy_error_recursive(self): | ||||
|         """Test Policy error which would cause recursion""" | ||||
|         transport = NotificationTransport.objects.create(name="transport") | ||||
|         NotificationRule.objects.filter(name__startswith="default").delete() | ||||
|         trigger = NotificationRule.objects.create(name="trigger", group=self.group) | ||||
|         trigger.transports.add(transport) | ||||
|         trigger.save() | ||||
|         matcher = EventMatcherPolicy.objects.create( | ||||
|             name="matcher", action=EventAction.CUSTOM_PREFIX | ||||
|         ) | ||||
|         PolicyBinding.objects.create(target=trigger, policy=matcher, order=0) | ||||
|  | ||||
|         execute_mock = MagicMock() | ||||
|         passes = MagicMock(side_effect=PolicyException) | ||||
|         with patch( | ||||
|             "authentik.policies.event_matcher.models.EventMatcherPolicy.passes", passes | ||||
|         ): | ||||
|             with patch( | ||||
|                 "authentik.events.models.NotificationTransport.send", execute_mock | ||||
|             ): | ||||
|                 Event.new(EventAction.CUSTOM_PREFIX).save() | ||||
|         self.assertEqual(passes.call_count, 0) | ||||
							
								
								
									
										98
									
								
								authentik/events/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								authentik/events/utils.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,98 @@ | ||||
| """event utilities""" | ||||
| import re | ||||
| from dataclasses import asdict, is_dataclass | ||||
| from typing import Any, Dict, Optional | ||||
| from uuid import UUID | ||||
|  | ||||
| from django.contrib.auth.models import AnonymousUser | ||||
| from django.core.handlers.wsgi import WSGIRequest | ||||
| from django.db import models | ||||
| from django.db.models.base import Model | ||||
| from django.http.request import HttpRequest | ||||
| from django.views.debug import SafeExceptionReporterFilter | ||||
| from guardian.utils import get_anonymous_user | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.policies.types import PolicyRequest | ||||
|  | ||||
| # Special keys which are *not* cleaned, even when the default filter | ||||
| # is matched | ||||
| ALLOWED_SPECIAL_KEYS = re.compile("passing", flags=re.I) | ||||
|  | ||||
|  | ||||
| def cleanse_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||
|     """Cleanse a dictionary, recursively""" | ||||
|     final_dict = {} | ||||
|     for key, value in source.items(): | ||||
|         try: | ||||
|             if SafeExceptionReporterFilter.hidden_settings.search( | ||||
|                 key | ||||
|             ) and not ALLOWED_SPECIAL_KEYS.search(key): | ||||
|                 final_dict[key] = SafeExceptionReporterFilter.cleansed_substitute | ||||
|             else: | ||||
|                 final_dict[key] = value | ||||
|         except TypeError:  # pragma: no cover | ||||
|             final_dict[key] = value | ||||
|         if isinstance(value, dict): | ||||
|             final_dict[key] = cleanse_dict(value) | ||||
|     return final_dict | ||||
|  | ||||
|  | ||||
| def model_to_dict(model: Model) -> Dict[str, Any]: | ||||
|     """Convert model to dict""" | ||||
|     name = str(model) | ||||
|     if hasattr(model, "name"): | ||||
|         name = model.name | ||||
|     return { | ||||
|         "app": model._meta.app_label, | ||||
|         "model_name": model._meta.model_name, | ||||
|         "pk": model.pk, | ||||
|         "name": name, | ||||
|     } | ||||
|  | ||||
|  | ||||
| def get_user(user: User, original_user: Optional[User] = None) -> Dict[str, Any]: | ||||
|     """Convert user object to dictionary, optionally including the original user""" | ||||
|     if isinstance(user, AnonymousUser): | ||||
|         user = get_anonymous_user() | ||||
|     user_data = { | ||||
|         "username": user.username, | ||||
|         "pk": user.pk, | ||||
|         "email": user.email, | ||||
|     } | ||||
|     if original_user: | ||||
|         original_data = get_user(original_user) | ||||
|         original_data["on_behalf_of"] = user_data | ||||
|         return original_data | ||||
|     return user_data | ||||
|  | ||||
|  | ||||
| def sanitize_dict(source: Dict[Any, Any]) -> Dict[Any, Any]: | ||||
|     """clean source of all Models that would interfere with the JSONField. | ||||
|     Models are replaced with a dictionary of { | ||||
|         app: str, | ||||
|         name: str, | ||||
|         pk: Any | ||||
|     }""" | ||||
|     final_dict = {} | ||||
|     for key, value in source.items(): | ||||
|         if is_dataclass(value): | ||||
|             # Because asdict calls `copy.deepcopy(obj)` on everything thats not tuple/dict, | ||||
|             # and deepcopy doesn't work with HttpRequests (neither django nor rest_framework). | ||||
|             # Currently, the only dataclass that actually holds an http request is a PolicyRequest | ||||
|             if isinstance(value, PolicyRequest): | ||||
|                 value.http_request = None | ||||
|             value = asdict(value) | ||||
|         if isinstance(value, dict): | ||||
|             final_dict[key] = sanitize_dict(value) | ||||
|         elif isinstance(value, User): | ||||
|             final_dict[key] = sanitize_dict(get_user(value)) | ||||
|         elif isinstance(value, models.Model): | ||||
|             final_dict[key] = sanitize_dict(model_to_dict(value)) | ||||
|         elif isinstance(value, UUID): | ||||
|             final_dict[key] = value.hex | ||||
|         elif isinstance(value, (HttpRequest, WSGIRequest)): | ||||
|             continue | ||||
|         else: | ||||
|             final_dict[key] = value | ||||
|     return final_dict | ||||
| @ -1,9 +1,17 @@ | ||||
| """Flow API Views""" | ||||
| from dataclasses import dataclass | ||||
|  | ||||
| from django.core.cache import cache | ||||
| from django.db.models import Model | ||||
| from django.shortcuts import get_object_or_404 | ||||
| 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.mixins import ListModelMixin | ||||
| from rest_framework.request import Request | ||||
| from rest_framework.response import Response | ||||
| from rest_framework.serializers import ( | ||||
|     CharField, | ||||
|     ModelSerializer, | ||||
|     Serializer, | ||||
|     SerializerMethodField, | ||||
| @ -40,12 +48,110 @@ class FlowSerializer(ModelSerializer): | ||||
|         ] | ||||
|  | ||||
|  | ||||
| class FlowDiagramSerializer(Serializer): | ||||
|     """response of the flow's /diagram/ action""" | ||||
|  | ||||
|     diagram = CharField(read_only=True) | ||||
|  | ||||
|     def create(self, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     def update(self, instance: Model, validated_data: dict) -> Model: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|  | ||||
| @dataclass | ||||
| class DiagramElement: | ||||
|     """Single element used in a diagram""" | ||||
|  | ||||
|     identifier: str | ||||
|     type: str | ||||
|     rest: str | ||||
|  | ||||
|     def __str__(self) -> str: | ||||
|         return f"{self.identifier}=>{self.type}: {self.rest}" | ||||
|  | ||||
|  | ||||
| class FlowViewSet(ModelViewSet): | ||||
|     """Flow Viewset""" | ||||
|  | ||||
|     queryset = Flow.objects.all() | ||||
|     serializer_class = FlowSerializer | ||||
|     lookup_field = "slug" | ||||
|     search_fields = ["name", "slug", "designation", "title"] | ||||
|     filterset_fields = ["flow_uuid", "name", "slug", "designation"] | ||||
|  | ||||
|     @swagger_auto_schema(responses={200: FlowDiagramSerializer()}) | ||||
|     @action(detail=True, methods=["get"]) | ||||
|     def diagram(self, request: Request, slug: str) -> Response: | ||||
|         """Return diagram for flow with slug `slug`, in the format used by flowchart.js""" | ||||
|         flow = get_object_or_404( | ||||
|             get_objects_for_user(request.user, "authentik_flows.view_flow").filter( | ||||
|                 slug=slug | ||||
|             ) | ||||
|         ) | ||||
|         header = [ | ||||
|             DiagramElement("st", "start", "Start"), | ||||
|         ] | ||||
|         body: list[DiagramElement] = [] | ||||
|         footer = [] | ||||
|         # First, collect all elements we need | ||||
|         for s_index, stage_binding in enumerate( | ||||
|             get_objects_for_user(request.user, "authentik_flows.view_flowstagebinding") | ||||
|             .filter(target=flow) | ||||
|             .order_by("order") | ||||
|         ): | ||||
|             body.append( | ||||
|                 DiagramElement( | ||||
|                     f"stage_{s_index}", | ||||
|                     "operation", | ||||
|                     f"Stage\n{stage_binding.stage.name}", | ||||
|                 ) | ||||
|             ) | ||||
|             for p_index, policy_binding in enumerate( | ||||
|                 get_objects_for_user( | ||||
|                     request.user, "authentik_policies.view_policybinding" | ||||
|                 ) | ||||
|                 .filter(target=stage_binding) | ||||
|                 .order_by("order") | ||||
|             ): | ||||
|                 body.append( | ||||
|                     DiagramElement( | ||||
|                         f"stage_{s_index}_policy_{p_index}", | ||||
|                         "condition", | ||||
|                         f"Policy\n{policy_binding.policy.name}", | ||||
|                     ) | ||||
|                 ) | ||||
|         # If the 2nd last element is a policy, we need to have an item to point to | ||||
|         # for a negative case | ||||
|         body.append( | ||||
|             DiagramElement("e", "end", "End|future"), | ||||
|         ) | ||||
|         if len(body) == 1: | ||||
|             footer.append("st(right)->e") | ||||
|         else: | ||||
|             # Actual diagram flow | ||||
|             footer.append(f"st(right)->{body[0].identifier}") | ||||
|             for index in range(len(body) - 1): | ||||
|                 element: DiagramElement = body[index] | ||||
|                 if element.type == "condition": | ||||
|                     # Policy passes, link policy yes to next stage | ||||
|                     footer.append( | ||||
|                         f"{element.identifier}(yes, right)->{body[index + 1].identifier}" | ||||
|                     ) | ||||
|                     # Policy doesn't pass, go to stage after next stage | ||||
|                     no_element = body[index + 1] | ||||
|                     if no_element.type != "end": | ||||
|                         no_element = body[index + 2] | ||||
|                     footer.append( | ||||
|                         f"{element.identifier}(no, bottom)->{no_element.identifier}" | ||||
|                     ) | ||||
|                 elif element.type == "operation": | ||||
|                     footer.append( | ||||
|                         f"{element.identifier}(bottom)->{body[index + 1].identifier}" | ||||
|                     ) | ||||
|         diagram = "\n".join([str(x) for x in header + body + footer]) | ||||
|         return Response({"diagram": diagram}) | ||||
|  | ||||
|  | ||||
| class StageSerializer(ModelSerializer): | ||||
|  | ||||
| @ -7,7 +7,7 @@ from time import time | ||||
| from django import db | ||||
| from django.core.management.base import BaseCommand | ||||
| from django.test import RequestFactory | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik import __version__ | ||||
| from authentik.core.models import User | ||||
|  | ||||
| @ -3,7 +3,7 @@ from dataclasses import dataclass | ||||
| from typing import TYPE_CHECKING, Optional | ||||
|  | ||||
| from django.http.request import HttpRequest | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.flows.models import Stage | ||||
|  | ||||
| @ -8,7 +8,7 @@ from django.http import HttpRequest | ||||
| from django.utils.translation import gettext_lazy as _ | ||||
| from model_utils.managers import InheritanceManager | ||||
| from rest_framework.serializers import BaseSerializer | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.lib.models import InheritanceForeignKey, SerializerModel | ||||
| from authentik.policies.models import PolicyBindingModel | ||||
|  | ||||
| @ -6,10 +6,10 @@ from django.core.cache import cache | ||||
| from django.http import HttpRequest | ||||
| from sentry_sdk.hub import Hub | ||||
| from sentry_sdk.tracing import Span | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.audit.models import cleanse_dict | ||||
| from authentik.core.models import User | ||||
| from authentik.events.models import cleanse_dict | ||||
| from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from authentik.flows.markers import ReevaluateMarker, StageMarker | ||||
| from authentik.flows.models import Flow, FlowStageBinding, Stage | ||||
| @ -21,6 +21,7 @@ PLAN_CONTEXT_PENDING_USER = "pending_user" | ||||
| PLAN_CONTEXT_SSO = "is_sso" | ||||
| PLAN_CONTEXT_REDIRECT = "redirect" | ||||
| PLAN_CONTEXT_APPLICATION = "application" | ||||
| PLAN_CONTEXT_SOURCE = "source" | ||||
|  | ||||
|  | ||||
| def cache_key(flow: Flow, user: Optional[User] = None) -> str: | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
| from django.core.cache import cache | ||||
| from django.db.models.signals import post_save | ||||
| from django.dispatch import receiver | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
|  | ||||
							
								
								
									
										92
									
								
								authentik/flows/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								authentik/flows/tests/test_api.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,92 @@ | ||||
| """API flow tests""" | ||||
| from django.shortcuts import reverse | ||||
| from rest_framework.test import APITestCase | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.flows.api import StageSerializer, StageViewSet | ||||
| from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage | ||||
| from authentik.policies.dummy.models import DummyPolicy | ||||
| from authentik.policies.models import PolicyBinding | ||||
| from authentik.stages.dummy.models import DummyStage | ||||
|  | ||||
| DIAGRAM_EXPECTED = """st=>start: Start | ||||
| stage_0=>operation: Stage | ||||
| dummy1 | ||||
| stage_1=>operation: Stage | ||||
| dummy2 | ||||
| stage_1_policy_0=>condition: Policy | ||||
| None | ||||
| e=>end: End|future | ||||
| st(right)->stage_0 | ||||
| stage_0(bottom)->stage_1 | ||||
| stage_1(bottom)->stage_1_policy_0 | ||||
| stage_1_policy_0(yes, right)->e | ||||
| stage_1_policy_0(no, bottom)->e""" | ||||
| DIAGRAM_SHORT_EXPECTED = """st=>start: Start | ||||
| e=>end: End|future | ||||
| st(right)->e""" | ||||
|  | ||||
|  | ||||
| class TestFlowsAPI(APITestCase): | ||||
|     """API tests""" | ||||
|  | ||||
|     def test_models(self): | ||||
|         """Test that ui_user_settings returns none""" | ||||
|         self.assertIsNone(Stage().ui_user_settings) | ||||
|  | ||||
|     def test_api_serializer(self): | ||||
|         """Test that stage serializer returns the correct type""" | ||||
|         obj = DummyStage() | ||||
|         self.assertEqual(StageSerializer().get_type(obj), "dummy") | ||||
|         self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage") | ||||
|  | ||||
|     def test_api_viewset(self): | ||||
|         """Test that stage serializer returns the correct type""" | ||||
|         dummy = DummyStage.objects.create() | ||||
|         self.assertIn(dummy, StageViewSet().get_queryset()) | ||||
|  | ||||
|     def test_api_diagram(self): | ||||
|         """Test flow diagram.""" | ||||
|         user = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(user) | ||||
|  | ||||
|         flow = Flow.objects.create( | ||||
|             name="test-default-context", | ||||
|             slug="test-default-context", | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         false_policy = DummyPolicy.objects.create(result=False, wait_min=1, wait_max=2) | ||||
|  | ||||
|         FlowStageBinding.objects.create( | ||||
|             target=flow, stage=DummyStage.objects.create(name="dummy1"), order=0 | ||||
|         ) | ||||
|         binding2 = FlowStageBinding.objects.create( | ||||
|             target=flow, | ||||
|             stage=DummyStage.objects.create(name="dummy2"), | ||||
|             order=1, | ||||
|             re_evaluate_policies=True, | ||||
|         ) | ||||
|  | ||||
|         PolicyBinding.objects.create(policy=false_policy, target=binding2, order=0) | ||||
|  | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:flow-diagram", kwargs={"slug": flow.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual(response.content, {"diagram": DIAGRAM_EXPECTED}) | ||||
|  | ||||
|     def test_api_diagram_no_stages(self): | ||||
|         """Test flow diagram with no stages.""" | ||||
|         user = User.objects.get(username="akadmin") | ||||
|         self.client.force_login(user) | ||||
|  | ||||
|         flow = Flow.objects.create( | ||||
|             name="test-default-context", | ||||
|             slug="test-default-context", | ||||
|             designation=FlowDesignation.AUTHENTICATION, | ||||
|         ) | ||||
|         response = self.client.get( | ||||
|             reverse("authentik_api:flow-diagram", kwargs={"slug": flow.slug}) | ||||
|         ) | ||||
|         self.assertEqual(response.status_code, 200) | ||||
|         self.assertJSONEqual(response.content, {"diagram": DIAGRAM_SHORT_EXPECTED}) | ||||
| @ -1,25 +0,0 @@ | ||||
| """miscellaneous flow tests""" | ||||
| from django.test import TestCase | ||||
|  | ||||
| from authentik.flows.api import StageSerializer, StageViewSet | ||||
| from authentik.flows.models import Stage | ||||
| from authentik.stages.dummy.models import DummyStage | ||||
|  | ||||
|  | ||||
| class TestFlowsMisc(TestCase): | ||||
|     """miscellaneous tests""" | ||||
|  | ||||
|     def test_models(self): | ||||
|         """Test that ui_user_settings returns none""" | ||||
|         self.assertIsNone(Stage().ui_user_settings) | ||||
|  | ||||
|     def test_api_serializer(self): | ||||
|         """Test that stage serializer returns the correct type""" | ||||
|         obj = DummyStage() | ||||
|         self.assertEqual(StageSerializer().get_type(obj), "dummy") | ||||
|         self.assertEqual(StageSerializer().get_verbose_name(obj), "Dummy Stage") | ||||
|  | ||||
|     def test_api_viewset(self): | ||||
|         """Test that stage serializer returns the correct type""" | ||||
|         dummy = DummyStage.objects.create() | ||||
|         self.assertIn(dummy, StageViewSet().get_queryset()) | ||||
| @ -15,10 +15,10 @@ 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 structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.audit.models import cleanse_dict | ||||
| from authentik.core.models import USER_ATTRIBUTE_DEBUG | ||||
| from authentik.events.models import cleanse_dict | ||||
| from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException | ||||
| from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage | ||||
| from authentik.flows.planner import ( | ||||
|  | ||||
| @ -5,13 +5,15 @@ from contextlib import contextmanager | ||||
| from glob import glob | ||||
| from json import dumps | ||||
| from time import time | ||||
| from typing import Any, Dict | ||||
| from typing import Any | ||||
| from urllib.parse import urlparse | ||||
|  | ||||
| import yaml | ||||
| from django.conf import ImproperlyConfigured | ||||
| from django.http import HttpRequest | ||||
|  | ||||
| from authentik import __version__ | ||||
|  | ||||
| SEARCH_PATHS = ["authentik/lib/default.yml", "/etc/authentik/config.yml", ""] + glob( | ||||
|     "/etc/authentik/config.d/*.yml", recursive=True | ||||
| ) | ||||
| @ -19,10 +21,9 @@ ENV_PREFIX = "AUTHENTIK" | ||||
| ENVIRONMENT = os.getenv(f"{ENV_PREFIX}_ENV", "local") | ||||
|  | ||||
|  | ||||
| def context_processor(request: HttpRequest) -> Dict[str, Any]: | ||||
| def context_processor(request: HttpRequest) -> dict[str, Any]: | ||||
|     """Context Processor that injects config object into every template""" | ||||
|     kwargs = {"config": CONFIG.raw} | ||||
|     return kwargs | ||||
|     return {"config": CONFIG.raw, "ak_version": __version__} | ||||
|  | ||||
|  | ||||
| class ConfigLoader: | ||||
|  | ||||
| @ -21,6 +21,17 @@ error_reporting: | ||||
|   environment: customer | ||||
|   send_pii: false | ||||
|  | ||||
| # Global email settings | ||||
| email: | ||||
|   host: localhost | ||||
|   port: 25 | ||||
|   username: "" | ||||
|   password: "" | ||||
|   use_tls: false | ||||
|   use_ssl: false | ||||
|   timeout: 10 | ||||
|   from: authentik@localhost | ||||
|  | ||||
| outposts: | ||||
|   docker_image_base: "beryju/authentik" # this is prepended to -proxy:version | ||||
|  | ||||
|  | ||||
| @ -7,7 +7,7 @@ from django.core.exceptions import ValidationError | ||||
| from requests import Session | ||||
| from sentry_sdk.hub import Hub | ||||
| from sentry_sdk.tracing import Span | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
|  | ||||
| @ -80,11 +80,15 @@ class BaseEvaluator: | ||||
|             span: Span | ||||
|             span.set_data("expression", expression_source) | ||||
|             param_keys = self._context.keys() | ||||
|             ast_obj = compile( | ||||
|                 self.wrap_expression(expression_source, param_keys), | ||||
|                 self._filename, | ||||
|                 "exec", | ||||
|             ) | ||||
|             try: | ||||
|                 ast_obj = compile( | ||||
|                     self.wrap_expression(expression_source, param_keys), | ||||
|                     self._filename, | ||||
|                     "exec", | ||||
|                 ) | ||||
|             except (SyntaxError, ValueError) as exc: | ||||
|                 self.handle_error(exc, expression_source) | ||||
|                 raise exc | ||||
|             try: | ||||
|                 _locals = self._context | ||||
|                 # Yes this is an exec, yes it is potentially bad. Since we limit what variables are | ||||
| @ -94,10 +98,15 @@ class BaseEvaluator: | ||||
|                 exec(ast_obj, self._globals, _locals)  # nosec # noqa | ||||
|                 result = _locals["result"] | ||||
|             except Exception as exc: | ||||
|                 LOGGER.warning("Expression error", exc=exc) | ||||
|                 raise | ||||
|                 self.handle_error(exc, expression_source) | ||||
|                 raise exc | ||||
|             return result | ||||
|  | ||||
|     # pylint: disable=unused-argument | ||||
|     def handle_error(self, exc: Exception, expression_source: str):  # pragma: no cover | ||||
|         """Exception Handler""" | ||||
|         LOGGER.warning("Expression error", exc=exc) | ||||
|  | ||||
|     def validate(self, expression: str) -> bool: | ||||
|         """Validate expression's syntax, raise ValidationError if Syntax is invalid""" | ||||
|         param_keys = self._context.keys() | ||||
|  | ||||
| @ -11,7 +11,7 @@ from ldap3.core.exceptions import LDAPException | ||||
| from redis.exceptions import ConnectionError as RedisConnectionError | ||||
| from redis.exceptions import RedisError, ResponseError | ||||
| from rest_framework.exceptions import APIException | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
| from websockets.exceptions import WebSocketException | ||||
|  | ||||
| LOGGER = get_logger() | ||||
|  | ||||
| @ -52,6 +52,11 @@ class TaskInfo: | ||||
|  | ||||
|     task_description: Optional[str] = field(default=None) | ||||
|  | ||||
|     @property | ||||
|     def html_name(self) -> list[str]: | ||||
|         """Get task_name, but split on underscores, so we can join in the html template.""" | ||||
|         return self.task_name.split("_") | ||||
|  | ||||
|     @staticmethod | ||||
|     def all() -> Dict[str, "TaskInfo"]: | ||||
|         """Get all TaskInfo objects""" | ||||
|  | ||||
| @ -8,7 +8,7 @@ from django.http.request import HttpRequest | ||||
| from django.template import Context | ||||
| from django.templatetags.static import static | ||||
| from django.utils.html import escape, mark_safe | ||||
| from structlog import get_logger | ||||
| from structlog.stdlib import get_logger | ||||
|  | ||||
| from authentik.core.models import User | ||||
| from authentik.lib.config import CONFIG | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user
	