Compare commits
	
		
			15 Commits
		
	
	
		
			version/0.
			...
			version/0.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 65d9f690cd | |||
| f96c2db5df | |||
| 5647f53140 | |||
| 4e20cd0fee | |||
| 49636f8fa0 | |||
| cd8157ea08 | |||
| 2a94ad7782 | |||
| 07eb5ffb4b | |||
| 8cc68928b8 | |||
| 221db12f85 | |||
| 34166d3c20 | |||
| 94972d64e6 | |||
| 253eaa382c | |||
| fc4f9733d1 | |||
| 8d784afcd1 | 
@ -1,5 +1,5 @@
 | 
				
			|||||||
[bumpversion]
 | 
					[bumpversion]
 | 
				
			||||||
current_version = 0.14.0-stable
 | 
					current_version = 0.13.5-stable
 | 
				
			||||||
tag = True
 | 
					tag = True
 | 
				
			||||||
commit = True
 | 
					commit = True
 | 
				
			||||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
					parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-(?P<release>.*)
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@ -18,11 +18,11 @@ jobs:
 | 
				
			|||||||
      - name: Building Docker Image
 | 
					      - name: Building Docker Image
 | 
				
			||||||
        run: docker build
 | 
					        run: docker build
 | 
				
			||||||
          --no-cache
 | 
					          --no-cache
 | 
				
			||||||
          -t beryju/authentik:0.14.0-stable
 | 
					          -t beryju/authentik:0.13.5-stable
 | 
				
			||||||
          -t beryju/authentik:latest
 | 
					          -t beryju/authentik:latest
 | 
				
			||||||
          -f Dockerfile .
 | 
					          -f Dockerfile .
 | 
				
			||||||
      - name: Push Docker Container to Registry (versioned)
 | 
					      - name: Push Docker Container to Registry (versioned)
 | 
				
			||||||
        run: docker push beryju/authentik:0.14.0-stable
 | 
					        run: docker push beryju/authentik:0.13.5-stable
 | 
				
			||||||
      - name: Push Docker Container to Registry (latest)
 | 
					      - name: Push Docker Container to Registry (latest)
 | 
				
			||||||
        run: docker push beryju/authentik:latest
 | 
					        run: docker push beryju/authentik:latest
 | 
				
			||||||
  build-proxy:
 | 
					  build-proxy:
 | 
				
			||||||
@ -48,11 +48,11 @@ jobs:
 | 
				
			|||||||
          cd proxy/
 | 
					          cd proxy/
 | 
				
			||||||
          docker build \
 | 
					          docker build \
 | 
				
			||||||
          --no-cache \
 | 
					          --no-cache \
 | 
				
			||||||
          -t beryju/authentik-proxy:0.14.0-stable \
 | 
					          -t beryju/authentik-proxy:0.13.5-stable \
 | 
				
			||||||
          -t beryju/authentik-proxy:latest \
 | 
					          -t beryju/authentik-proxy:latest \
 | 
				
			||||||
          -f Dockerfile .
 | 
					          -f Dockerfile .
 | 
				
			||||||
      - name: Push Docker Container to Registry (versioned)
 | 
					      - name: Push Docker Container to Registry (versioned)
 | 
				
			||||||
        run: docker push beryju/authentik-proxy:0.14.0-stable
 | 
					        run: docker push beryju/authentik-proxy:0.13.5-stable
 | 
				
			||||||
      - name: Push Docker Container to Registry (latest)
 | 
					      - name: Push Docker Container to Registry (latest)
 | 
				
			||||||
        run: docker push beryju/authentik-proxy:latest
 | 
					        run: docker push beryju/authentik-proxy:latest
 | 
				
			||||||
  build-static:
 | 
					  build-static:
 | 
				
			||||||
@ -69,11 +69,11 @@ jobs:
 | 
				
			|||||||
          cd web/
 | 
					          cd web/
 | 
				
			||||||
          docker build \
 | 
					          docker build \
 | 
				
			||||||
          --no-cache \
 | 
					          --no-cache \
 | 
				
			||||||
          -t beryju/authentik-static:0.14.0-stable \
 | 
					          -t beryju/authentik-static:0.13.5-stable \
 | 
				
			||||||
          -t beryju/authentik-static:latest \
 | 
					          -t beryju/authentik-static:latest \
 | 
				
			||||||
          -f Dockerfile .
 | 
					          -f Dockerfile .
 | 
				
			||||||
      - name: Push Docker Container to Registry (versioned)
 | 
					      - name: Push Docker Container to Registry (versioned)
 | 
				
			||||||
        run: docker push beryju/authentik-static:0.14.0-stable
 | 
					        run: docker push beryju/authentik-static:0.13.5-stable
 | 
				
			||||||
      - name: Push Docker Container to Registry (latest)
 | 
					      - name: Push Docker Container to Registry (latest)
 | 
				
			||||||
        run: docker push beryju/authentik-static:latest
 | 
					        run: docker push beryju/authentik-static:latest
 | 
				
			||||||
  test-release:
 | 
					  test-release:
 | 
				
			||||||
@ -107,5 +107,5 @@ jobs:
 | 
				
			|||||||
          SENTRY_PROJECT: authentik
 | 
					          SENTRY_PROJECT: authentik
 | 
				
			||||||
          SENTRY_URL: https://sentry.beryju.org
 | 
					          SENTRY_URL: https://sentry.beryju.org
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          tagName: 0.14.0-stable
 | 
					          tagName: 0.13.5-stable
 | 
				
			||||||
          environment: beryjuorg-prod
 | 
					          environment: beryjuorg-prod
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										695
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										695
									
								
								LICENSE
									
									
									
									
									
								
							@ -1,674 +1,21 @@
 | 
				
			|||||||
                    GNU GENERAL PUBLIC LICENSE
 | 
					MIT 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
 | 
					Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
				
			||||||
 of this license document, but changing it is not allowed.
 | 
					of this software and associated documentation files (the "Software"), to deal
 | 
				
			||||||
 | 
					in the Software without restriction, including without limitation the rights
 | 
				
			||||||
                            Preamble
 | 
					to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 | 
				
			||||||
 | 
					copies of the Software, and to permit persons to whom the Software is
 | 
				
			||||||
  The GNU General Public License is a free, copyleft license for
 | 
					furnished to do so, subject to the following conditions:
 | 
				
			||||||
software and other kinds of works.
 | 
					
 | 
				
			||||||
 | 
					The above copyright notice and this permission notice shall be included in all
 | 
				
			||||||
  The licenses for most software and other practical works are designed
 | 
					copies or substantial portions of the Software.
 | 
				
			||||||
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
 | 
					THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 | 
				
			||||||
share and change all versions of a program--to make sure it remains free
 | 
					IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 | 
				
			||||||
software for all its users.  We, the Free Software Foundation, use the
 | 
					FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 | 
				
			||||||
GNU General Public License for most of our software; it applies also to
 | 
					AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 | 
				
			||||||
any other work released this way by its authors.  You can apply it to
 | 
					LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 | 
				
			||||||
your programs, too.
 | 
					OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 | 
				
			||||||
 | 
					SOFTWARE.
 | 
				
			||||||
  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>.
 | 
					 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										195
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										195
									
								
								Pipfile.lock
									
									
									
										generated
									
									
									
								
							@ -53,10 +53,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "autobahn": {
 | 
					        "autobahn": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:410a93e0e29882c8b5d5ab05d220b07609b886ef5f23c0b8d39153254ffd6895",
 | 
					                "sha256:491238c31f78721eaa9d0593909ab455a4ea68127aadd76ecf67185143f5f298",
 | 
				
			||||||
                "sha256:52ee4236ff9a1fcbbd9500439dcf3284284b37f8a6b31ecc8a36e00cf9f95049"
 | 
					                "sha256:72b68a1ce1e10e3cbcc3b280aae86d5b2e7a1f409febab1ab91a8a3274113f6e"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==20.12.3"
 | 
					            "version": "==20.12.2"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "automat": {
 | 
					        "automat": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -74,18 +74,18 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "boto3": {
 | 
					        "boto3": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:0bb2c3159b9f5e0df50430bf06a155bd7f27f480825b6374dde807d42360a668",
 | 
					                "sha256:a05614300fd404c7952a55ae92e106b9400ae65886425aaab3104527be833848",
 | 
				
			||||||
                "sha256:a49b3ab4bfa2f6394ba60165cfc468410797dd410f32eed47e22f61451ee986e"
 | 
					                "sha256:c7556b0861d982b71043fbc0df024644320c817ad796391c442d0c2f15a77223"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.16.43"
 | 
					            "version": "==1.16.39"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "botocore": {
 | 
					        "botocore": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:7398c900dbd4e3d61647269215396ea3e8082f494f3e7b65d9b6aca049c1d463",
 | 
					                "sha256:449e4196160ff58ee27d2a626a7ce4cfff2640fe1806d7a279e73a30ad286347",
 | 
				
			||||||
                "sha256:795a67338cadb0c3a45014a6c81659da6af623a4e973812f87a6f9d9fb7712e9"
 | 
					                "sha256:e0d0386098a072abd7b6c087e6149d997377c969a823ebe01b3f5bfabe9bfac0"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==1.19.43"
 | 
					            "version": "==1.19.39"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "cachetools": {
 | 
					        "cachetools": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -343,11 +343,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "django-storages": {
 | 
					        "django-storages": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:c823dbf56c9e35b0999a13d7e05062b837bae36c518a40255d522fbe3750fbb4",
 | 
					                "sha256:056ec3e9e2b0c6f363913976072ffba2923e79e4859578047da139ba1637497e",
 | 
				
			||||||
                "sha256:f28765826d507a0309cfaa849bd084894bc71d81bf0d09479168d44785396f80"
 | 
					                "sha256:7af56611c62a1c174aab4e862efb7fdd98296dccf76f42135f5b6851fc313c97"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==1.11.1"
 | 
					            "version": "==1.11"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "djangorestframework": {
 | 
					        "djangorestframework": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -366,11 +366,11 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "docker": {
 | 
					        "docker": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:0604a74719d5d2de438753934b755bfcda6f62f49b8e4b30969a4b0a2a8a1220",
 | 
					                "sha256:317e95a48c32de8c1aac92a48066a5b73e218ed096e03758bcdd799a7130a1a1",
 | 
				
			||||||
                "sha256:e455fa49aabd4f22da9f4e1c1f9d16308286adc60abaf64bf3e1feafaed81d06"
 | 
					                "sha256:cffc771d4ea1389fc66bc95cb72d304aa41d1a1563482a9a000fba3a84ed5071"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==4.4.1"
 | 
					            "version": "==4.4.0"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "drf-yasg2": {
 | 
					        "drf-yasg2": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -646,36 +646,46 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "msgpack": {
 | 
					        "msgpack": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:0cb94ee48675a45d3b86e61d13c1e6f1696f0183f0715544976356ff86f741d9",
 | 
					                "sha256:01835e300967e5ad6fdbfc36eafe74df67ff47e16e0d6dee8766630550315903",
 | 
				
			||||||
                "sha256:1026dcc10537d27dd2d26c327e552f05ce148977e9d7b9f1718748281b38c841",
 | 
					                "sha256:03c5554315317d76c25a15569dd52ac6047b105df71e861f24faf9675672b72d",
 | 
				
			||||||
                "sha256:26a1759f1a88df5f1d0b393eb582ec022326994e311ba9c5818adc5374736439",
 | 
					                "sha256:0968b368a9a9081435bfcb7a57a1e8b75c7bf038ef911b369acd2e038c7f873a",
 | 
				
			||||||
                "sha256:2a5866bdc88d77f6e1370f82f2371c9bc6fc92fe898fa2dec0c5d4f5435a2694",
 | 
					                "sha256:1d7ab166401f7789bf11262439336c0a01b878f0d602e48f35c35d2e3a555820",
 | 
				
			||||||
                "sha256:31c17bbf2ae5e29e48d794c693b7ca7a0c73bd4280976d408c53df421e838d2a",
 | 
					                "sha256:1e8d27bac821f8aa909904a704a67e5e8bc2e42b153415fc3621b7afbc06702b",
 | 
				
			||||||
                "sha256:497d2c12426adcd27ab83144057a705efb6acc7e85957a51d43cdcf7f258900f",
 | 
					                "sha256:1fc9f21da9fd77088ebfd3c9941b044ca3f4a048e85f7ff5727f26bcdbffed61",
 | 
				
			||||||
                "sha256:5a9ee2540c78659a1dd0b110f73773533ee3108d4e1219b5a15a8d635b7aca0e",
 | 
					                "sha256:20196229acc193939223118c7420838749d5b0cece49cd397739a3a6ffcfe2d1",
 | 
				
			||||||
                "sha256:8521e5be9e3b93d4d5e07cb80b7e32353264d143c1f072309e1863174c6aadb1",
 | 
					                "sha256:2933443313289725f16bd7b99a8c3aa6a2cca1549e661d7407f056a0af80bf7b",
 | 
				
			||||||
                "sha256:87869ba567fe371c4555d2e11e4948778ab6b59d6cc9d8460d543e4cfbbddd1c",
 | 
					                "sha256:2966b155356fd231fa441131d7301e1596ee38974ad56dc57fd752fdbe2bb63f",
 | 
				
			||||||
                "sha256:8ffb24a3b7518e843cd83538cf859e026d24ec41ac5721c18ed0c55101f9775b",
 | 
					                "sha256:29a6fb3729215b6fcab786ef4f460a5406a5c056f7021191f70ff7712a3f6ba4",
 | 
				
			||||||
                "sha256:92be4b12de4806d3c36810b0fe2aeedd8d493db39e2eb90742b9c09299eb5759",
 | 
					                "sha256:35cbefa7d7bddfb4b0770a1b9ff721cd8dfe9a680947a68457974d5e3e6acc2f",
 | 
				
			||||||
                "sha256:9ea52fff0473f9f3000987f313310208c879493491ef3ccf66268eff8d5a0326",
 | 
					                "sha256:35ff1ac162a77fb78be360d9f771d36cbf1202e94fc6d70e284ad5db6ab72608",
 | 
				
			||||||
                "sha256:a4355d2193106c7aa77c98fc955252a737d8550320ecdb2e9ac701e15e2943bc",
 | 
					                "sha256:40dd1ac7420f071e96b3e4a4a7b8e69546a6f8065ff5995dbacf53f86207eb98",
 | 
				
			||||||
                "sha256:a99b144475230982aee16b3d249170f1cccebf27fb0a08e9f603b69637a62192",
 | 
					                "sha256:4bea1938e484c9caca9585105f447d6807c496c153b7244fa726b3cc4a68ec9e",
 | 
				
			||||||
                "sha256:ac25f3e0513f6673e8b405c3a80500eb7be1cf8f57584be524c4fa78fe8e0c83",
 | 
					                "sha256:4e58b9f4a99bc3a90859bb006ec4422448a5ce39e5cd6e7498c56de5dcec9c34",
 | 
				
			||||||
                "sha256:b28c0876cce1466d7c2195d7658cf50e4730667196e2f1355c4209444717ee06",
 | 
					                "sha256:66d47e952856bfcde46d8351380d0b5b928a73112b66bc06d5367dfcc077c06a",
 | 
				
			||||||
                "sha256:b55f7db883530b74c857e50e149126b91bb75d35c08b28db12dcb0346f15e46e",
 | 
					                "sha256:69f6aa503378548ea1e760c11aeb6fc91952bf3634fd806a38a0e47edb507fcd",
 | 
				
			||||||
                "sha256:b6d9e2dae081aa35c44af9c4298de4ee72991305503442a5c74656d82b581fe9",
 | 
					                "sha256:7033215267a0e9f60f4a5e4fb2228a932c404f237817caff8dc3115d9e7cd975",
 | 
				
			||||||
                "sha256:c747c0cc08bd6d72a586310bda6ea72eeb28e7505990f342552315b229a19b33",
 | 
					                "sha256:7b50afd767cc053ad92fad39947c3670db27305fd1c49acded44d9d9ac8b56fd",
 | 
				
			||||||
                "sha256:d6c64601af8f3893d17ec233237030e3110f11b8a962cb66720bf70c0141aa54",
 | 
					                "sha256:99ea9e65876546743b2b8bb5bc7adefbb03b9da78a899827467da197a48f790b",
 | 
				
			||||||
                "sha256:d8167b84af26654c1124857d71650404336f4eb5cc06900667a493fc619ddd9f",
 | 
					                "sha256:abcc62303ac4d789878d4aac4cdba1bbe2adb478d67be99cd4a6d56ac3a4028f",
 | 
				
			||||||
                "sha256:de6bd7990a2c2dabe926b7e62a92886ccbf809425c347ae7de277067f97c2887",
 | 
					                "sha256:b107f9b36665bf7d7c6176a938a361a7aba16aa179d833919448f77287866484",
 | 
				
			||||||
                "sha256:e36a812ef4705a291cdb4a2fd352f013134f26c6ff63477f20235138d1d21009",
 | 
					                "sha256:b5b27923b6c98a2616b7e906a29e4e10e1b4424aea87a0e0d5636327dc6ea315",
 | 
				
			||||||
                "sha256:e89ec55871ed5473a041c0495b7b4e6099f6263438e0bd04ccd8418f92d5d7f2",
 | 
					                "sha256:bf8eedc7bfbf63cbc9abe58287c32d78780a347835e82c23033c68f11f80bb05",
 | 
				
			||||||
                "sha256:f3e6aaf217ac1c7ce1563cf52a2f4f5d5b1f64e8729d794165db71da57257f0c",
 | 
					                "sha256:c144ff4954a6ea40aa603600c8be175349588fc68696092889fa34ab6e055060",
 | 
				
			||||||
                "sha256:f484cd2dca68502de3704f056fa9b318c94b1539ed17a4c784266df5d6978c87",
 | 
					                "sha256:c4e5f96a1d0d916ce7a16decb7499e8923ddef007cf7d68412fb68767766648a",
 | 
				
			||||||
                "sha256:fae04496f5bc150eefad4e9571d1a76c55d021325dcd484ce45065ebbdd00984",
 | 
					                "sha256:c60e8b2bf754b8dcc1075c5bee0b177ed9193e7cbd2377faaf507120a948e697",
 | 
				
			||||||
                "sha256:fe07bc6735d08e492a327f496b7850e98cb4d112c56df69b0c844dbebcbb47f6"
 | 
					                "sha256:c82fc6cdba5737eb6ed0c926a30a5d56e7b050297375a16d6c5ad89b576fd979",
 | 
				
			||||||
 | 
					                "sha256:ce4ebe2c79411cd5671b20862831880e7850a2de699cff6626f48853fde61ae6",
 | 
				
			||||||
 | 
					                "sha256:d113c6b1239c62669ef3063693842605a3edbfebc39a333cf91ba60d314afe6d",
 | 
				
			||||||
 | 
					                "sha256:d3cea07ad16919a44e8d1ea67efa5244855cdce807d672f41694acc24d08834e",
 | 
				
			||||||
 | 
					                "sha256:d76672602db16e3f44bc1a85c7ee5f15d79e02fcf5bc9d133c2954753be6eddc",
 | 
				
			||||||
 | 
					                "sha256:decf2091b75987ca2564e3b742f9614eb7d57e39ff04eaa68af7a3fc5648f7ed",
 | 
				
			||||||
 | 
					                "sha256:e13b9007af66a3f62574bc0a13843df0e4402f5ee4b00a02aa1803f01d26b9fb",
 | 
				
			||||||
 | 
					                "sha256:e157edf4213dacafb0f862e0b7a3a18448250cec91aa1334f432f49028acc650",
 | 
				
			||||||
 | 
					                "sha256:e234ff83628ca3ab345bf97fb36ccbf6d2f1700f5e08868643bf4489edc960f8",
 | 
				
			||||||
 | 
					                "sha256:f08d9dd3ce0c5e972dc4653f0fb66d2703941e65356388c13032b578dd718261",
 | 
				
			||||||
 | 
					                "sha256:f20d7d4f1f0728560408ba6933154abccf0c20f24642a2404b43d5c23e4119ab"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==1.0.2"
 | 
					            "version": "==1.0.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "oauthlib": {
 | 
					        "oauthlib": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1054,10 +1064,10 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "txaio": {
 | 
					        "txaio": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:1488d31d564a116538cc1265ac3f7979fb6223bb5a9e9f1479436ee2c17d8549",
 | 
					                "sha256:17938f2bca4a9cabce61346758e482ca4e600160cbc28e861493eac74a19539d",
 | 
				
			||||||
                "sha256:a8676d6c68aea1f0e2548c4afdb8e6253873af3bc2659bb5bcd9f39dff7ff90f"
 | 
					                "sha256:38a469daf93c37e5527cb062653d6393ae11663147c42fab7ddc3f6d00d434ae"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "version": "==20.12.1"
 | 
					            "version": "==20.4.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "uritemplate": {
 | 
					        "uritemplate": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1083,11 +1093,11 @@
 | 
				
			|||||||
                "standard"
 | 
					                "standard"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:6707fa7f4dbd86fd6982a2d4ecdaad2704e4514d23a1e4278104311288b04691",
 | 
					                "sha256:2a7b17f4d9848d6557ccc2274a5f7c97f1daf037d130a0c6918f67cd9bc8cdf5",
 | 
				
			||||||
                "sha256:d19ca083bebd212843e01f689900e5c637a292c63bb336c7f0735a99300a5f38"
 | 
					                "sha256:6fcce74c00b77d4f4b3ed7ba1b2a370d27133bfdb46f835b7a76dfe0a8c110ae"
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "index": "pypi",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==0.13.2"
 | 
					            "version": "==0.13.1"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "uvloop": {
 | 
					        "uvloop": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
@ -1318,58 +1328,43 @@
 | 
				
			|||||||
        },
 | 
					        },
 | 
				
			||||||
        "coverage": {
 | 
					        "coverage": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
                "sha256:08b3ba72bd981531fd557f67beee376d6700fba183b167857038997ba30dd297",
 | 
					                "sha256:0203acd33d2298e19b57451ebb0bed0ab0c602e5cf5a818591b4918b1f97d516",
 | 
				
			||||||
                "sha256:2757fa64e11ec12220968f65d086b7a29b6583d16e9a544c889b22ba98555ef1",
 | 
					                "sha256:0f313707cdecd5cd3e217fc68c78a960b616604b559e9ea60cc16795c4304259",
 | 
				
			||||||
                "sha256:3102bb2c206700a7d28181dbe04d66b30780cde1d1c02c5f3c165cf3d2489497",
 | 
					                "sha256:1c6703094c81fa55b816f5ae542c6ffc625fec769f22b053adb42ad712d086c9",
 | 
				
			||||||
                "sha256:3498b27d8236057def41de3585f317abae235dd3a11d33e01736ffedb2ef8606",
 | 
					                "sha256:1d44bb3a652fed01f1f2c10d5477956116e9b391320c94d36c6bf13b088a1097",
 | 
				
			||||||
                "sha256:378ac77af41350a8c6b8801a66021b52da8a05fd77e578b7380e876c0ce4f528",
 | 
					                "sha256:280baa8ec489c4f542f8940f9c4c2181f0306a8ee1a54eceba071a449fb870a0",
 | 
				
			||||||
                "sha256:38f16b1317b8dd82df67ed5daa5f5e7c959e46579840d77a67a4ceb9cef0a50b",
 | 
					                "sha256:29a6272fec10623fcbe158fdf9abc7a5fa032048ac1d8631f14b50fbfc10d17f",
 | 
				
			||||||
                "sha256:3911c2ef96e5ddc748a3c8b4702c61986628bb719b8378bf1e4a6184bbd48fe4",
 | 
					                "sha256:2b31f46bf7b31e6aa690d4c7a3d51bb262438c6dcb0d528adde446531d0d3bb7",
 | 
				
			||||||
                "sha256:3a3c3f8863255f3c31db3889f8055989527173ef6192a283eb6f4db3c579d830",
 | 
					                "sha256:2d43af2be93ffbad25dd959899b5b809618a496926146ce98ee0b23683f8c51c",
 | 
				
			||||||
                "sha256:3b14b1da110ea50c8bcbadc3b82c3933974dbeea1832e814aab93ca1163cd4c1",
 | 
					                "sha256:381ead10b9b9af5f64646cd27107fb27b614ee7040bb1226f9c07ba96625cbb5",
 | 
				
			||||||
                "sha256:535dc1e6e68fad5355f9984d5637c33badbdc987b0c0d303ee95a6c979c9516f",
 | 
					                "sha256:47a11bdbd8ada9b7ee628596f9d97fbd3851bd9999d398e9436bd67376dbece7",
 | 
				
			||||||
                "sha256:6f61319e33222591f885c598e3e24f6a4be3533c1d70c19e0dc59e83a71ce27d",
 | 
					                "sha256:4d6a42744139a7fa5b46a264874a781e8694bb32f1d76d8137b68138686f1729",
 | 
				
			||||||
                "sha256:723d22d324e7997a651478e9c5a3120a0ecbc9a7e94071f7e1954562a8806cf3",
 | 
					                "sha256:50691e744714856f03a86df3e2bff847c2acede4c191f9a1da38f088df342978",
 | 
				
			||||||
                "sha256:76b2775dda7e78680d688daabcb485dc87cf5e3184a0b3e012e1d40e38527cc8",
 | 
					                "sha256:530cc8aaf11cc2ac7430f3614b04645662ef20c348dce4167c22d99bec3480e9",
 | 
				
			||||||
                "sha256:782a5c7df9f91979a7a21792e09b34a658058896628217ae6362088b123c8500",
 | 
					                "sha256:582ddfbe712025448206a5bc45855d16c2e491c2dd102ee9a2841418ac1c629f",
 | 
				
			||||||
                "sha256:7e4d159021c2029b958b2363abec4a11db0ce8cd43abb0d9ce44284cb97217e7",
 | 
					                "sha256:63808c30b41f3bbf65e29f7280bf793c79f54fb807057de7e5238ffc7cc4d7b9",
 | 
				
			||||||
                "sha256:8dacc4073c359f40fcf73aede8428c35f84639baad7e1b46fce5ab7a8a7be4bb",
 | 
					                "sha256:71b69bd716698fa62cd97137d6f2fdf49f534decb23a2c6fc80813e8b7be6822",
 | 
				
			||||||
                "sha256:8f33d1156241c43755137288dea619105477961cfa7e47f48dbf96bc2c30720b",
 | 
					                "sha256:7858847f2d84bf6e64c7f66498e851c54de8ea06a6f96a32a1d192d846734418",
 | 
				
			||||||
                "sha256:8ffd4b204d7de77b5dd558cdff986a8274796a1e57813ed005b33fd97e29f059",
 | 
					                "sha256:78e93cc3571fd928a39c0b26767c986188a4118edc67bc0695bc7a284da22e82",
 | 
				
			||||||
                "sha256:93a280c9eb736a0dcca19296f3c30c720cb41a71b1f9e617f341f0a8e791a69b",
 | 
					                "sha256:7f43286f13d91a34fadf61ae252a51a130223c52bfefb50310d5b2deb062cf0f",
 | 
				
			||||||
                "sha256:9a4f66259bdd6964d8cf26142733c81fb562252db74ea367d9beb4f815478e72",
 | 
					                "sha256:86e9f8cd4b0cdd57b4ae71a9c186717daa4c5a99f3238a8723f416256e0b064d",
 | 
				
			||||||
                "sha256:9a9d4ff06804920388aab69c5ea8a77525cf165356db70131616acd269e19b36",
 | 
					                "sha256:8f264ba2701b8c9f815b272ad568d555ef98dfe1576802ab3149c3629a9f2221",
 | 
				
			||||||
                "sha256:a2070c5affdb3a5e751f24208c5c4f3d5f008fa04d28731416e023c93b275277",
 | 
					                "sha256:9342dd70a1e151684727c9c91ea003b2fb33523bf19385d4554f7897ca0141d4",
 | 
				
			||||||
                "sha256:a4857f7e2bc6921dbd487c5c88b84f5633de3e7d416c4dc0bb70256775551a6c",
 | 
					                "sha256:9361de40701666b034c59ad9e317bae95c973b9ff92513dd0eced11c6adf2e21",
 | 
				
			||||||
                "sha256:a607ae05b6c96057ba86c811d9c43423f35e03874ffb03fbdcd45e0637e8b631",
 | 
					                "sha256:9669179786254a2e7e57f0ecf224e978471491d660aaca833f845b72a2df3709",
 | 
				
			||||||
                "sha256:a66ca3bdf21c653e47f726ca57f46ba7fc1f260ad99ba783acc3e58e3ebdb9ff",
 | 
					                "sha256:aac1ba0a253e17889550ddb1b60a2063f7474155465577caa2a3b131224cfd54",
 | 
				
			||||||
                "sha256:ab110c48bc3d97b4d19af41865e14531f300b482da21783fdaacd159251890e8",
 | 
					                "sha256:aef72eae10b5e3116bac6957de1df4d75909fc76d1499a53fb6387434b6bcd8d",
 | 
				
			||||||
                "sha256:b239711e774c8eb910e9b1ac719f02f5ae4bf35fa0420f438cdc3a7e4e7dd6ec",
 | 
					                "sha256:bd3166bb3b111e76a4f8e2980fa1addf2920a4ca9b2b8ca36a3bc3dedc618270",
 | 
				
			||||||
                "sha256:be0416074d7f253865bb67630cf7210cbc14eb05f4099cc0f82430135aaa7a3b",
 | 
					                "sha256:c1b78fb9700fc961f53386ad2fd86d87091e06ede5d118b8a50dea285a071c24",
 | 
				
			||||||
                "sha256:c46643970dff9f5c976c6512fd35768c4a3819f01f61169d8cdac3f9290903b7",
 | 
					                "sha256:c3888a051226e676e383de03bf49eb633cd39fc829516e5334e69b8d81aae751",
 | 
				
			||||||
                "sha256:c5ec71fd4a43b6d84ddb88c1df94572479d9a26ef3f150cef3dacefecf888105",
 | 
					                "sha256:c5f17ad25d2c1286436761b462e22b5020d83316f8e8fcb5deb2b3151f8f1d3a",
 | 
				
			||||||
                "sha256:c6e5174f8ca585755988bc278c8bb5d02d9dc2e971591ef4a1baabdf2d99589b",
 | 
					                "sha256:c851b35fc078389bc16b915a0a7c1d5923e12e2c5aeec58c52f4aa8085ac8237",
 | 
				
			||||||
                "sha256:c89b558f8a9a5a6f2cfc923c304d49f0ce629c3bd85cb442ca258ec20366394c",
 | 
					                "sha256:cb7df71de0af56000115eafd000b867d1261f786b5eebd88a0ca6360cccfaca7",
 | 
				
			||||||
                "sha256:cc44e3545d908ecf3e5773266c487ad1877be718d9dc65fc7eb6e7d14960985b",
 | 
					                "sha256:cedb2f9e1f990918ea061f28a0f0077a07702e3819602d3507e2ff98c8d20636",
 | 
				
			||||||
                "sha256:cc6f8246e74dd210d7e2b56c76ceaba1cc52b025cd75dbe96eb48791e0250e98",
 | 
					                "sha256:e8caf961e1b1a945db76f1b5fa9c91498d15f545ac0ababbe575cfab185d3bd8"
 | 
				
			||||||
                "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",
 | 
					            "index": "pypi",
 | 
				
			||||||
            "version": "==5.3.1"
 | 
					            "version": "==5.3"
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        "django": {
 | 
					        "django": {
 | 
				
			||||||
            "hashes": [
 | 
					            "hashes": [
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
<img src="https://goauthentik.io/img/icon_top_brand_colour.svg" height="250" alt="authentik logo">
 | 
					<img src="web/icons/icon_top_brand.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
 | 
					## Screenshots
 | 
				
			||||||
 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||

 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Development
 | 
					## Development
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,2 +1,2 @@
 | 
				
			|||||||
"""authentik"""
 | 
					"""authentik"""
 | 
				
			||||||
__version__ = "0.14.0-stable"
 | 
					__version__ = "0.13.5-stable"
 | 
				
			||||||
 | 
				
			|||||||
@ -4,9 +4,10 @@ from collections import Counter
 | 
				
			|||||||
from datetime import timedelta
 | 
					from datetime import timedelta
 | 
				
			||||||
from typing import Dict, List
 | 
					from typing import Dict, List
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db.models import Count, ExpressionWrapper, F, Model
 | 
					from django.db.models import Count, ExpressionWrapper, F
 | 
				
			||||||
from django.db.models.fields import DurationField
 | 
					from django.db.models.fields import DurationField
 | 
				
			||||||
from django.db.models.functions import ExtractHour
 | 
					from django.db.models.functions import ExtractHour
 | 
				
			||||||
 | 
					from django.http import response
 | 
				
			||||||
from django.utils.timezone import now
 | 
					from django.utils.timezone import now
 | 
				
			||||||
from drf_yasg2.utils import swagger_auto_schema
 | 
					from drf_yasg2.utils import swagger_auto_schema
 | 
				
			||||||
from rest_framework.fields import SerializerMethodField
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
@ -16,7 +17,7 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.serializers import Serializer
 | 
					from rest_framework.serializers import Serializer
 | 
				
			||||||
from rest_framework.viewsets import ViewSet
 | 
					from rest_framework.viewsets import ViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.audit.models import Event, EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:
 | 
					def get_events_per_1h(**filter_kwargs) -> List[Dict[str, int]]:
 | 
				
			||||||
@ -59,10 +60,10 @@ class AdministrationMetricsSerializer(Serializer):
 | 
				
			|||||||
        """Get failed logins per hour for the last 24 hours"""
 | 
					        """Get failed logins per hour for the last 24 hours"""
 | 
				
			||||||
        return get_events_per_1h(action=EventAction.LOGIN_FAILED)
 | 
					        return get_events_per_1h(action=EventAction.LOGIN_FAILED)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data: dict) -> Model:
 | 
					    def create(self, request: Request) -> response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, instance: Model, validated_data: dict) -> Model:
 | 
					    def update(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
from importlib import import_module
 | 
					from importlib import import_module
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib import messages
 | 
					from django.contrib import messages
 | 
				
			||||||
from django.db.models import Model
 | 
					 | 
				
			||||||
from django.http.response import Http404
 | 
					from django.http.response import Http404
 | 
				
			||||||
from django.utils.translation import gettext_lazy as _
 | 
					from django.utils.translation import gettext_lazy as _
 | 
				
			||||||
from drf_yasg2.utils import swagger_auto_schema
 | 
					from drf_yasg2.utils import swagger_auto_schema
 | 
				
			||||||
@ -27,10 +26,10 @@ class TaskSerializer(Serializer):
 | 
				
			|||||||
    status = IntegerField(source="result.status.value")
 | 
					    status = IntegerField(source="result.status.value")
 | 
				
			||||||
    messages = ListField(source="result.messages")
 | 
					    messages = ListField(source="result.messages")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data: dict) -> Model:
 | 
					    def create(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, instance: Model, validated_data: dict) -> Model:
 | 
					    def update(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
"""authentik administration overview"""
 | 
					"""authentik administration overview"""
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from django.db.models import Model
 | 
					 | 
				
			||||||
from drf_yasg2.utils import swagger_auto_schema
 | 
					from drf_yasg2.utils import swagger_auto_schema
 | 
				
			||||||
from packaging.version import parse
 | 
					from packaging.version import parse
 | 
				
			||||||
from rest_framework.fields import SerializerMethodField
 | 
					from rest_framework.fields import SerializerMethodField
 | 
				
			||||||
@ -40,10 +39,10 @@ class VersionSerializer(Serializer):
 | 
				
			|||||||
            self.get_version_latest(instance)
 | 
					            self.get_version_latest(instance)
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data: dict) -> Model:
 | 
					    def create(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, instance: Model, validated_data: dict) -> Model:
 | 
					    def update(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -52,7 +51,7 @@ class VersionViewSet(ListModelMixin, GenericViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    permission_classes = [IsAdminUser]
 | 
					    permission_classes = [IsAdminUser]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):  # pragma: no cover
 | 
					    def get_queryset(self):
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @swagger_auto_schema(responses={200: VersionSerializer(many=True)})
 | 
					    @swagger_auto_schema(responses={200: VersionSerializer(many=True)})
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ class WorkerViewSet(ListModelMixin, GenericViewSet):
 | 
				
			|||||||
    serializer_class = Serializer
 | 
					    serializer_class = Serializer
 | 
				
			||||||
    permission_classes = [IsAdminUser]
 | 
					    permission_classes = [IsAdminUser]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):  # pragma: no cover
 | 
					    def get_queryset(self):
 | 
				
			||||||
        return None
 | 
					        return None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def list(self, request: Request) -> Response:
 | 
					    def list(self, request: Request) -> Response:
 | 
				
			||||||
 | 
				
			|||||||
@ -14,6 +14,4 @@ SOURCE_SERIALIZER_FIELDS = [
 | 
				
			|||||||
    "enabled",
 | 
					    "enabled",
 | 
				
			||||||
    "authentication_flow",
 | 
					    "authentication_flow",
 | 
				
			||||||
    "enrollment_flow",
 | 
					    "enrollment_flow",
 | 
				
			||||||
    "verbose_name",
 | 
					 | 
				
			||||||
    "verbose_name_plural",
 | 
					 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,8 @@
 | 
				
			|||||||
"""authentik admin tasks"""
 | 
					"""authentik admin tasks"""
 | 
				
			||||||
from django.core.cache import cache
 | 
					from django.core.cache import cache
 | 
				
			||||||
from packaging.version import parse
 | 
					 | 
				
			||||||
from requests import RequestException, get
 | 
					from requests import RequestException, get
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
					from authentik.lib.tasks import MonitoredTask, TaskResult, TaskResultStatus
 | 
				
			||||||
from authentik.root.celery import CELERY_APP
 | 
					from authentik.root.celery import CELERY_APP
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,24 +19,12 @@ def update_latest_version(self: MonitoredTask):
 | 
				
			|||||||
        response.raise_for_status()
 | 
					        response.raise_for_status()
 | 
				
			||||||
        data = response.json()
 | 
					        data = response.json()
 | 
				
			||||||
        tag_name = data.get("tag_name")
 | 
					        tag_name = data.get("tag_name")
 | 
				
			||||||
        upstream_version = tag_name.split("/")[1]
 | 
					        cache.set(VERSION_CACHE_KEY, tag_name.split("/")[1], VERSION_CACHE_TIMEOUT)
 | 
				
			||||||
        cache.set(VERSION_CACHE_KEY, upstream_version, VERSION_CACHE_TIMEOUT)
 | 
					 | 
				
			||||||
        self.set_status(
 | 
					        self.set_status(
 | 
				
			||||||
            TaskResult(
 | 
					            TaskResult(
 | 
				
			||||||
                TaskResultStatus.SUCCESSFUL, ["Successfully updated latest Version"]
 | 
					                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:
 | 
					    except (RequestException, IndexError) as exc:
 | 
				
			||||||
        cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
 | 
					        cache.set(VERSION_CACHE_KEY, "0.0.0", VERSION_CACHE_TIMEOUT)
 | 
				
			||||||
        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
 | 
					        self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										131
									
								
								authentik/admin/templates/administration/application/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								authentik/admin/templates/administration/application/list.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,131 @@
 | 
				
			|||||||
 | 
					{% 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 %}
 | 
					                {% for flow in object_list %}
 | 
				
			||||||
                <tr role="row">
 | 
					                <tr role="row">
 | 
				
			||||||
                    <th role="columnheader">
 | 
					                    <th role="columnheader">
 | 
				
			||||||
                        <a href="/flows/{{ flow.slug }}">
 | 
					                        <a href="/flows/{{ flow.slug }}/">
 | 
				
			||||||
                            <div><code>{{ flow.slug }}</code></div>
 | 
					                            <div><code>{{ flow.slug }}</code></div>
 | 
				
			||||||
                            <small>{{ flow.name }}</small>
 | 
					                            <small>{{ flow.name }}</small>
 | 
				
			||||||
                        </a>
 | 
					                        </a>
 | 
				
			||||||
 | 
				
			|||||||
@ -41,17 +41,6 @@
 | 
				
			|||||||
                                </ak-modal-button>
 | 
					                                </ak-modal-button>
 | 
				
			||||||
                            </li>
 | 
					                            </li>
 | 
				
			||||||
                            {% endfor %}
 | 
					                            {% 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>
 | 
					                        </ul>
 | 
				
			||||||
                    </ak-dropdown>
 | 
					                    </ak-dropdown>
 | 
				
			||||||
                    <button role="ak-refresh" class="pf-c-button pf-m-primary">
 | 
					                    <button role="ak-refresh" class="pf-c-button pf-m-primary">
 | 
				
			||||||
 | 
				
			|||||||
@ -1,76 +0,0 @@
 | 
				
			|||||||
"""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/1.2.3"
 | 
					 | 
				
			||||||
        }""",
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
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), "1.2.3")
 | 
					 | 
				
			||||||
        self.assertTrue(
 | 
					 | 
				
			||||||
            Event.objects.filter(
 | 
					 | 
				
			||||||
                action=EventAction.UPDATE_AVAILABLE, context__new_version="1.2.3"
 | 
					 | 
				
			||||||
            ).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="1.2.3"
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
            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()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
@ -22,7 +22,6 @@ from authentik.admin.views import (
 | 
				
			|||||||
    tokens,
 | 
					    tokens,
 | 
				
			||||||
    users,
 | 
					    users,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.providers.saml.views import MetadataImportView
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
urlpatterns = [
 | 
					urlpatterns = [
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
@ -36,6 +35,9 @@ urlpatterns = [
 | 
				
			|||||||
        name="overview-clear-policy-cache",
 | 
					        name="overview-clear-policy-cache",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    # Applications
 | 
					    # Applications
 | 
				
			||||||
 | 
					    path(
 | 
				
			||||||
 | 
					        "applications/", applications.ApplicationListView.as_view(), name="applications"
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "applications/create/",
 | 
					        "applications/create/",
 | 
				
			||||||
        applications.ApplicationCreateView.as_view(),
 | 
					        applications.ApplicationCreateView.as_view(),
 | 
				
			||||||
@ -117,11 +119,6 @@ urlpatterns = [
 | 
				
			|||||||
        providers.ProviderCreateView.as_view(),
 | 
					        providers.ProviderCreateView.as_view(),
 | 
				
			||||||
        name="provider-create",
 | 
					        name="provider-create",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    path(
 | 
					 | 
				
			||||||
        "providers/create/saml/from-metadata/",
 | 
					 | 
				
			||||||
        MetadataImportView.as_view(),
 | 
					 | 
				
			||||||
        name="provider-saml-from-metadata",
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "providers/<int:pk>/update/",
 | 
					        "providers/<int:pk>/update/",
 | 
				
			||||||
        providers.ProviderUpdateView.as_view(),
 | 
					        providers.ProviderUpdateView.as_view(),
 | 
				
			||||||
 | 
				
			|||||||
@ -6,15 +6,44 @@ from django.contrib.auth.mixins import (
 | 
				
			|||||||
from django.contrib.messages.views import SuccessMessageMixin
 | 
					from django.contrib.messages.views import SuccessMessageMixin
 | 
				
			||||||
from django.urls import reverse_lazy
 | 
					from django.urls import reverse_lazy
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					from django.utils.translation import gettext as _
 | 
				
			||||||
from django.views.generic import UpdateView
 | 
					from django.views.generic import ListView, UpdateView
 | 
				
			||||||
from guardian.mixins import PermissionRequiredMixin
 | 
					from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.views.utils import BackSuccessUrlMixin, DeleteMessageView
 | 
					from authentik.admin.views.utils import (
 | 
				
			||||||
 | 
					    BackSuccessUrlMixin,
 | 
				
			||||||
 | 
					    DeleteMessageView,
 | 
				
			||||||
 | 
					    SearchListMixin,
 | 
				
			||||||
 | 
					    UserPaginateListMixin,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
from authentik.core.forms.applications import ApplicationForm
 | 
					from authentik.core.forms.applications import ApplicationForm
 | 
				
			||||||
from authentik.core.models import Application
 | 
					from authentik.core.models import Application
 | 
				
			||||||
from authentik.lib.views import CreateAssignPermView
 | 
					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(
 | 
					class ApplicationCreateView(
 | 
				
			||||||
    SuccessMessageMixin,
 | 
					    SuccessMessageMixin,
 | 
				
			||||||
    BackSuccessUrlMixin,
 | 
					    BackSuccessUrlMixin,
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,7 @@ from authentik.admin.views.utils import (
 | 
				
			|||||||
from authentik.lib.views import CreateAssignPermView
 | 
					from authentik.lib.views import CreateAssignPermView
 | 
				
			||||||
from authentik.stages.invitation.forms import InvitationForm
 | 
					from authentik.stages.invitation.forms import InvitationForm
 | 
				
			||||||
from authentik.stages.invitation.models import Invitation
 | 
					from authentik.stages.invitation.models import Invitation
 | 
				
			||||||
 | 
					from authentik.stages.invitation.signals import invitation_created
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InvitationListView(
 | 
					class InvitationListView(
 | 
				
			||||||
@ -58,6 +59,7 @@ class InvitationCreateView(
 | 
				
			|||||||
        obj = form.save(commit=False)
 | 
					        obj = form.save(commit=False)
 | 
				
			||||||
        obj.created_by = self.request.user
 | 
					        obj.created_by = self.request.user
 | 
				
			||||||
        obj.save()
 | 
					        obj.save()
 | 
				
			||||||
 | 
					        invitation_created.send(sender=self, request=self.request, invitation=obj)
 | 
				
			||||||
        return HttpResponseRedirect(self.success_url)
 | 
					        return HttpResponseRedirect(self.success_url)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
"""core Configs API"""
 | 
					"""core Configs API"""
 | 
				
			||||||
from django.db.models import Model
 | 
					 | 
				
			||||||
from drf_yasg2.utils import swagger_auto_schema
 | 
					from drf_yasg2.utils import swagger_auto_schema
 | 
				
			||||||
from rest_framework.permissions import AllowAny
 | 
					from rest_framework.permissions import AllowAny
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
@ -20,10 +19,10 @@ class ConfigSerializer(Serializer):
 | 
				
			|||||||
    error_reporting_environment = ReadOnlyField()
 | 
					    error_reporting_environment = ReadOnlyField()
 | 
				
			||||||
    error_reporting_send_pii = ReadOnlyField()
 | 
					    error_reporting_send_pii = ReadOnlyField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data: dict) -> Model:
 | 
					    def create(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, instance: Model, validated_data: dict) -> Model:
 | 
					    def update(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,5 @@
 | 
				
			|||||||
"""core messages API"""
 | 
					"""core messages API"""
 | 
				
			||||||
from django.contrib.messages import get_messages
 | 
					from django.contrib.messages import get_messages
 | 
				
			||||||
from django.db.models import Model
 | 
					 | 
				
			||||||
from drf_yasg2.utils import swagger_auto_schema
 | 
					from drf_yasg2.utils import swagger_auto_schema
 | 
				
			||||||
from rest_framework.permissions import AllowAny
 | 
					from rest_framework.permissions import AllowAny
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
@ -18,10 +17,10 @@ class MessageSerializer(Serializer):
 | 
				
			|||||||
    extra_tags = ReadOnlyField()
 | 
					    extra_tags = ReadOnlyField()
 | 
				
			||||||
    level_tag = ReadOnlyField()
 | 
					    level_tag = ReadOnlyField()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data: dict) -> Model:
 | 
					    def create(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self, instance: Model, validated_data: dict) -> Model:
 | 
					    def update(self, request: Request) -> Response:
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,7 @@ from authentik.admin.api.version import VersionViewSet
 | 
				
			|||||||
from authentik.admin.api.workers import WorkerViewSet
 | 
					from authentik.admin.api.workers import WorkerViewSet
 | 
				
			||||||
from authentik.api.v2.config import ConfigsViewSet
 | 
					from authentik.api.v2.config import ConfigsViewSet
 | 
				
			||||||
from authentik.api.v2.messages import MessagesViewSet
 | 
					from authentik.api.v2.messages import MessagesViewSet
 | 
				
			||||||
 | 
					from authentik.audit.api import EventViewSet
 | 
				
			||||||
from authentik.core.api.applications import ApplicationViewSet
 | 
					from authentik.core.api.applications import ApplicationViewSet
 | 
				
			||||||
from authentik.core.api.groups import GroupViewSet
 | 
					from authentik.core.api.groups import GroupViewSet
 | 
				
			||||||
from authentik.core.api.propertymappings import PropertyMappingViewSet
 | 
					from authentik.core.api.propertymappings import PropertyMappingViewSet
 | 
				
			||||||
@ -19,7 +20,6 @@ from authentik.core.api.sources import SourceViewSet
 | 
				
			|||||||
from authentik.core.api.tokens import TokenViewSet
 | 
					from authentik.core.api.tokens import TokenViewSet
 | 
				
			||||||
from authentik.core.api.users import UserViewSet
 | 
					from authentik.core.api.users import UserViewSet
 | 
				
			||||||
from authentik.crypto.api import CertificateKeyPairViewSet
 | 
					from authentik.crypto.api import CertificateKeyPairViewSet
 | 
				
			||||||
from authentik.events.api import EventViewSet
 | 
					 | 
				
			||||||
from authentik.flows.api import (
 | 
					from authentik.flows.api import (
 | 
				
			||||||
    FlowCacheViewSet,
 | 
					    FlowCacheViewSet,
 | 
				
			||||||
    FlowStageBindingViewSet,
 | 
					    FlowStageBindingViewSet,
 | 
				
			||||||
@ -96,7 +96,7 @@ router.register("flows/bindings", FlowStageBindingViewSet)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
 | 
					router.register("crypto/certificatekeypairs", CertificateKeyPairViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("events/events", EventViewSet)
 | 
					router.register("audit/events", EventViewSet)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
router.register("sources/all", SourceViewSet)
 | 
					router.register("sources/all", SourceViewSet)
 | 
				
			||||||
router.register("sources/ldap", LDAPSourceViewSet)
 | 
					router.register("sources/ldap", LDAPSourceViewSet)
 | 
				
			||||||
@ -148,9 +148,7 @@ info = openapi.Info(
 | 
				
			|||||||
    title="authentik API",
 | 
					    title="authentik API",
 | 
				
			||||||
    default_version="v2",
 | 
					    default_version="v2",
 | 
				
			||||||
    contact=openapi.Contact(email="hello@beryju.org"),
 | 
					    contact=openapi.Contact(email="hello@beryju.org"),
 | 
				
			||||||
    license=openapi.License(
 | 
					    license=openapi.License(name="MIT License"),
 | 
				
			||||||
        name="GNU GPLv3", url="https://github.com/BeryJu/authentik/blob/master/LICENSE"
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
SchemaView = get_schema_view(
 | 
					SchemaView = get_schema_view(
 | 
				
			||||||
    info,
 | 
					    info,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""Events API Views"""
 | 
					"""Audit API Views"""
 | 
				
			||||||
from django.db.models.aggregates import Count
 | 
					from django.db.models.aggregates import Count
 | 
				
			||||||
from django.db.models.fields.json import KeyTextTransform
 | 
					from django.db.models.fields.json import KeyTextTransform
 | 
				
			||||||
from drf_yasg2.utils import swagger_auto_schema
 | 
					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.serializers import ModelSerializer, Serializer
 | 
				
			||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
					from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					from authentik.audit.models import Event, EventAction
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class EventSerializer(ModelSerializer):
 | 
					class EventSerializer(ModelSerializer):
 | 
				
			||||||
@ -48,15 +48,6 @@ class EventViewSet(ReadOnlyModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = Event.objects.all()
 | 
					    queryset = Event.objects.all()
 | 
				
			||||||
    serializer_class = EventSerializer
 | 
					    serializer_class = EventSerializer
 | 
				
			||||||
    ordering = ["-created"]
 | 
					 | 
				
			||||||
    search_fields = [
 | 
					 | 
				
			||||||
        "user",
 | 
					 | 
				
			||||||
        "action",
 | 
					 | 
				
			||||||
        "app",
 | 
					 | 
				
			||||||
        "context",
 | 
					 | 
				
			||||||
        "client_ip",
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    filterset_fields = ["action"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @swagger_auto_schema(
 | 
					    @swagger_auto_schema(
 | 
				
			||||||
        method="GET", responses={200: EventTopPerUserSerialier(many=True)}
 | 
					        method="GET", responses={200: EventTopPerUserSerialier(many=True)}
 | 
				
			||||||
							
								
								
									
										16
									
								
								authentik/audit/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								authentik/audit/apps.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					"""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,4 +1,4 @@
 | 
				
			|||||||
"""Events middleware"""
 | 
					"""Audit middleware"""
 | 
				
			||||||
from functools import partial
 | 
					from functools import partial
 | 
				
			||||||
from typing import Callable
 | 
					from typing import Callable
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -7,10 +7,9 @@ from django.db.models import Model
 | 
				
			|||||||
from django.db.models.signals import post_save, pre_delete
 | 
					from django.db.models.signals import post_save, pre_delete
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import Event, EventAction, model_to_dict
 | 
				
			||||||
 | 
					from authentik.audit.signals import EventNewThread
 | 
				
			||||||
from authentik.core.middleware import LOCAL
 | 
					from authentik.core.middleware import LOCAL
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.events.signals import EventNewThread
 | 
					 | 
				
			||||||
from authentik.events.utils import model_to_dict
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuditMiddleware:
 | 
					class AuditMiddleware:
 | 
				
			||||||
@ -63,8 +63,8 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            options={
 | 
					            options={
 | 
				
			||||||
                "verbose_name": "Event",
 | 
					                "verbose_name": "Audit Event",
 | 
				
			||||||
                "verbose_name_plural": "Events",
 | 
					                "verbose_name_plural": "Audit Events",
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
@ -6,7 +6,7 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("authentik_events", "0001_initial"),
 | 
					        ("authentik_audit", "0001_initial"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
@ -3,11 +3,11 @@ from django.apps.registry import Apps
 | 
				
			|||||||
from django.db import migrations, models
 | 
					from django.db import migrations, models
 | 
				
			||||||
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
					from django.db.backends.base.schema import BaseDatabaseSchemaEditor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import authentik.events.models
 | 
					import authentik.audit.models
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
					def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			||||||
    Event = apps.get_model("authentik_events", "Event")
 | 
					    Event = apps.get_model("authentik_audit", "Event")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    db_alias = schema_editor.connection.alias
 | 
					    db_alias = schema_editor.connection.alias
 | 
				
			||||||
    for event in Event.objects.all():
 | 
					    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
 | 
					        # Because event objects cannot be updated, we have to re-create them
 | 
				
			||||||
        event.pk = None
 | 
					        event.pk = None
 | 
				
			||||||
        event.user_json = (
 | 
					        event.user_json = (
 | 
				
			||||||
            authentik.events.models.get_user(event.user) if event.user else {}
 | 
					            authentik.audit.models.get_user(event.user) if event.user else {}
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        event._state.adding = True
 | 
					        event._state.adding = True
 | 
				
			||||||
        event.save()
 | 
					        event.save()
 | 
				
			||||||
@ -24,7 +24,7 @@ def convert_user_to_json(apps: Apps, schema_editor: BaseDatabaseSchemaEditor):
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("authentik_events", "0002_auto_20200918_2116"),
 | 
					        ("authentik_audit", "0002_auto_20200918_2116"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
@ -6,7 +6,7 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("authentik_events", "0003_auto_20200917_1155"),
 | 
					        ("authentik_audit", "0003_auto_20200917_1155"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
@ -6,7 +6,7 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("authentik_events", "0004_auto_20200921_1829"),
 | 
					        ("authentik_audit", "0004_auto_20200921_1829"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
@ -6,7 +6,7 @@ from django.db import migrations, models
 | 
				
			|||||||
class Migration(migrations.Migration):
 | 
					class Migration(migrations.Migration):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    dependencies = [
 | 
					    dependencies = [
 | 
				
			||||||
        ("authentik_events", "0005_auto_20201005_2139"),
 | 
					        ("authentik_audit", "0005_auto_20201005_2139"),
 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    operations = [
 | 
					    operations = [
 | 
				
			||||||
@ -1,14 +1,17 @@
 | 
				
			|||||||
"""authentik events models"""
 | 
					"""authentik audit models"""
 | 
				
			||||||
 | 
					 | 
				
			||||||
from inspect import getmodule, stack
 | 
					from inspect import getmodule, stack
 | 
				
			||||||
from typing import Optional, Union
 | 
					from typing import Any, Dict, Optional, Union
 | 
				
			||||||
from uuid import uuid4
 | 
					from uuid import UUID, uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
 | 
					from django.contrib.auth.models import AnonymousUser
 | 
				
			||||||
from django.core.exceptions import ValidationError
 | 
					from django.core.exceptions import ValidationError
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
 | 
					from django.db.models.base import Model
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from django.utils.translation import gettext as _
 | 
					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 structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.middleware import (
 | 
					from authentik.core.middleware import (
 | 
				
			||||||
@ -16,14 +19,78 @@ from authentik.core.middleware import (
 | 
				
			|||||||
    SESSION_IMPERSONATE_USER,
 | 
					    SESSION_IMPERSONATE_USER,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.utils import cleanse_dict, get_user, sanitize_dict
 | 
					 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger("authentik.events")
 | 
					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):
 | 
					class EventAction(models.TextChoices):
 | 
				
			||||||
    """All possible actions to save into the events log"""
 | 
					    """All possible actions to save into the audit log"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    LOGIN = "login"
 | 
					    LOGIN = "login"
 | 
				
			||||||
    LOGIN_FAILED = "login_failed"
 | 
					    LOGIN_FAILED = "login_failed"
 | 
				
			||||||
@ -35,6 +102,7 @@ class EventAction(models.TextChoices):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    TOKEN_VIEW = "token_view"  # nosec
 | 
					    TOKEN_VIEW = "token_view"  # nosec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    INVITE_CREATED = "invitation_created"
 | 
				
			||||||
    INVITE_USED = "invitation_used"
 | 
					    INVITE_USED = "invitation_used"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    AUTHORIZE_APPLICATION = "authorize_application"
 | 
					    AUTHORIZE_APPLICATION = "authorize_application"
 | 
				
			||||||
@ -43,23 +111,15 @@ class EventAction(models.TextChoices):
 | 
				
			|||||||
    IMPERSONATION_STARTED = "impersonation_started"
 | 
					    IMPERSONATION_STARTED = "impersonation_started"
 | 
				
			||||||
    IMPERSONATION_ENDED = "impersonation_ended"
 | 
					    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_CREATED = "model_created"
 | 
				
			||||||
    MODEL_UPDATED = "model_updated"
 | 
					    MODEL_UPDATED = "model_updated"
 | 
				
			||||||
    MODEL_DELETED = "model_deleted"
 | 
					    MODEL_DELETED = "model_deleted"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    UPDATE_AVAILABLE = "update_available"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    CUSTOM_PREFIX = "custom_"
 | 
					    CUSTOM_PREFIX = "custom_"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Event(models.Model):
 | 
					class Event(models.Model):
 | 
				
			||||||
    """An individual Audit/Metrics/Notification/Error Event"""
 | 
					    """An individual audit log event"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
					    event_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
 | 
				
			||||||
    user = models.JSONField(default=dict)
 | 
					    user = models.JSONField(default=dict)
 | 
				
			||||||
@ -91,12 +151,6 @@ class Event(models.Model):
 | 
				
			|||||||
        event = Event(action=action, app=app, context=cleaned_kwargs)
 | 
					        event = Event(action=action, app=app, context=cleaned_kwargs)
 | 
				
			||||||
        return event
 | 
					        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(
 | 
					    def from_http(
 | 
				
			||||||
        self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
 | 
					        self, request: HttpRequest, user: Optional[settings.AUTH_USER_MODEL] = None
 | 
				
			||||||
    ) -> "Event":
 | 
					    ) -> "Event":
 | 
				
			||||||
@ -131,7 +185,7 @@ class Event(models.Model):
 | 
				
			|||||||
                "you may not edit an existing %s" % self._meta.model_name
 | 
					                "you may not edit an existing %s" % self._meta.model_name
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "Created Event",
 | 
					            "Created Audit event",
 | 
				
			||||||
            action=self.action,
 | 
					            action=self.action,
 | 
				
			||||||
            context=self.context,
 | 
					            context=self.context,
 | 
				
			||||||
            client_ip=self.client_ip,
 | 
					            client_ip=self.client_ip,
 | 
				
			||||||
@ -141,5 +195,5 @@ class Event(models.Model):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        verbose_name = _("Event")
 | 
					        verbose_name = _("Audit Event")
 | 
				
			||||||
        verbose_name_plural = _("Events")
 | 
					        verbose_name_plural = _("Audit Events")
 | 
				
			||||||
@ -1,4 +1,4 @@
 | 
				
			|||||||
"""authentik events signal listener"""
 | 
					"""authentik audit signal listener"""
 | 
				
			||||||
from threading import Thread
 | 
					from threading import Thread
 | 
				
			||||||
from typing import Any, Dict, Optional
 | 
					from typing import Any, Dict, Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -10,11 +10,11 @@ from django.contrib.auth.signals import (
 | 
				
			|||||||
from django.dispatch import receiver
 | 
					from django.dispatch import receiver
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import Event, EventAction
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.core.signals import password_changed
 | 
					from authentik.core.signals import password_changed
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.stages.invitation.models import Invitation
 | 
					from authentik.stages.invitation.models import Invitation
 | 
				
			||||||
from authentik.stages.invitation.signals import invitation_used
 | 
					from authentik.stages.invitation.signals import invitation_created, invitation_used
 | 
				
			||||||
from authentik.stages.user_write.signals import user_write
 | 
					from authentik.stages.user_write.signals import user_write
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -79,6 +79,16 @@ def on_user_login_failed(
 | 
				
			|||||||
    thread.run()
 | 
					    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)
 | 
					@receiver(invitation_used)
 | 
				
			||||||
# pylint: disable=unused-argument
 | 
					# pylint: disable=unused-argument
 | 
				
			||||||
def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
 | 
					def on_invitation_used(sender, request: HttpRequest, invitation: Invitation, **_):
 | 
				
			||||||
							
								
								
									
										90
									
								
								authentik/audit/templates/audit/list.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								authentik/audit/templates/audit/list.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					{% 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,15 +1,15 @@
 | 
				
			|||||||
"""events event tests"""
 | 
					"""audit event tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.contrib.contenttypes.models import ContentType
 | 
					from django.contrib.contenttypes.models import ContentType
 | 
				
			||||||
from django.test import TestCase
 | 
					from django.test import TestCase
 | 
				
			||||||
from guardian.shortcuts import get_anonymous_user
 | 
					from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event
 | 
					from authentik.audit.models import Event
 | 
				
			||||||
from authentik.policies.dummy.models import DummyPolicy
 | 
					from authentik.policies.dummy.models import DummyPolicy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestEvents(TestCase):
 | 
					class TestAuditEvent(TestCase):
 | 
				
			||||||
    """Test Event"""
 | 
					    """Test Audit Event"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_new_with_model(self):
 | 
					    def test_new_with_model(self):
 | 
				
			||||||
        """Create a new Event passing a model as kwarg"""
 | 
					        """Create a new Event passing a model as kwarg"""
 | 
				
			||||||
							
								
								
									
										9
									
								
								authentik/audit/urls.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								authentik/audit/urls.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					"""authentik audit urls"""
 | 
				
			||||||
 | 
					from django.urls import path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.views import EventListView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					urlpatterns = [
 | 
				
			||||||
 | 
					    # Audit Log
 | 
				
			||||||
 | 
					    path("audit/", EventListView.as_view(), name="log"),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
							
								
								
									
										30
									
								
								authentik/audit/views.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								authentik/audit/views.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					"""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",
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
@ -12,9 +12,9 @@ from rest_framework.viewsets import ModelViewSet
 | 
				
			|||||||
from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
					from rest_framework_guardian.filters import ObjectPermissionsFilter
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.api.metrics import get_events_per_1h
 | 
					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.api.providers import ProviderSerializer
 | 
				
			||||||
from authentik.core.models import Application
 | 
					from authentik.core.models import Application
 | 
				
			||||||
from authentik.events.models import EventAction
 | 
					 | 
				
			||||||
from authentik.policies.engine import PolicyEngine
 | 
					from authentik.policies.engine import PolicyEngine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -50,15 +50,7 @@ class ApplicationViewSet(ModelViewSet):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    queryset = Application.objects.all()
 | 
					    queryset = Application.objects.all()
 | 
				
			||||||
    serializer_class = ApplicationSerializer
 | 
					    serializer_class = ApplicationSerializer
 | 
				
			||||||
    search_fields = [
 | 
					 | 
				
			||||||
        "name",
 | 
					 | 
				
			||||||
        "slug",
 | 
					 | 
				
			||||||
        "meta_launch_url",
 | 
					 | 
				
			||||||
        "meta_description",
 | 
					 | 
				
			||||||
        "meta_publisher",
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
    lookup_field = "slug"
 | 
					    lookup_field = "slug"
 | 
				
			||||||
    ordering = ["name"]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
 | 
					    def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
 | 
				
			||||||
        """Custom filter_queryset method which ignores guardian, but still supports sorting"""
 | 
					        """Custom filter_queryset method which ignores guardian, but still supports sorting"""
 | 
				
			||||||
@ -73,7 +65,7 @@ class ApplicationViewSet(ModelViewSet):
 | 
				
			|||||||
        queryset = self._filter_queryset_for_list(self.get_queryset())
 | 
					        queryset = self._filter_queryset_for_list(self.get_queryset())
 | 
				
			||||||
        self.paginate_queryset(queryset)
 | 
					        self.paginate_queryset(queryset)
 | 
				
			||||||
        allowed_applications = []
 | 
					        allowed_applications = []
 | 
				
			||||||
        for application in queryset:
 | 
					        for application in queryset.order_by("name"):
 | 
				
			||||||
            engine = PolicyEngine(application, self.request.user, self.request)
 | 
					            engine = PolicyEngine(application, self.request.user, self.request)
 | 
				
			||||||
            engine.build()
 | 
					            engine.build()
 | 
				
			||||||
            if engine.passing:
 | 
					            if engine.passing:
 | 
				
			||||||
@ -88,7 +80,7 @@ class ApplicationViewSet(ModelViewSet):
 | 
				
			|||||||
            get_objects_for_user(request.user, "authentik_core.view_application"),
 | 
					            get_objects_for_user(request.user, "authentik_core.view_application"),
 | 
				
			||||||
            slug=slug,
 | 
					            slug=slug,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        if not request.user.has_perm("authentik_events.view_event"):
 | 
					        if not request.user.has_perm("authentik_audit.view_event"):
 | 
				
			||||||
            raise Http404
 | 
					            raise Http404
 | 
				
			||||||
        return Response(
 | 
					        return Response(
 | 
				
			||||||
            get_events_per_1h(
 | 
					            get_events_per_1h(
 | 
				
			||||||
 | 
				
			|||||||
@ -2,16 +2,15 @@
 | 
				
			|||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
					from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.api.utils import MetaNameSerializer
 | 
					 | 
				
			||||||
from authentik.core.models import Provider
 | 
					from authentik.core.models import Provider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProviderSerializer(ModelSerializer, MetaNameSerializer):
 | 
					class ProviderSerializer(ModelSerializer):
 | 
				
			||||||
    """Provider Serializer"""
 | 
					    """Provider Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    object_type = SerializerMethodField()
 | 
					    __type__ = SerializerMethodField(method_name="get_type")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object_type(self, obj):
 | 
					    def get_type(self, obj):
 | 
				
			||||||
        """Get object type so that we know which API Endpoint to use to get the full object"""
 | 
					        """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", "")
 | 
					        return obj._meta.object_name.lower().replace("provider", "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -30,9 +29,7 @@ class ProviderSerializer(ModelSerializer, MetaNameSerializer):
 | 
				
			|||||||
            "application",
 | 
					            "application",
 | 
				
			||||||
            "authorization_flow",
 | 
					            "authorization_flow",
 | 
				
			||||||
            "property_mappings",
 | 
					            "property_mappings",
 | 
				
			||||||
            "object_type",
 | 
					            "__type__",
 | 
				
			||||||
            "verbose_name",
 | 
					 | 
				
			||||||
            "verbose_name_plural",
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -3,11 +3,10 @@ from rest_framework.serializers import ModelSerializer, SerializerMethodField
 | 
				
			|||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
					from rest_framework.viewsets import ReadOnlyModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
 | 
					from authentik.admin.forms.source import SOURCE_SERIALIZER_FIELDS
 | 
				
			||||||
from authentik.core.api.utils import MetaNameSerializer
 | 
					 | 
				
			||||||
from authentik.core.models import Source
 | 
					from authentik.core.models import Source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SourceSerializer(ModelSerializer, MetaNameSerializer):
 | 
					class SourceSerializer(ModelSerializer):
 | 
				
			||||||
    """Source Serializer"""
 | 
					    """Source Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    __type__ = SerializerMethodField(method_name="get_type")
 | 
					    __type__ = SerializerMethodField(method_name="get_type")
 | 
				
			||||||
 | 
				
			|||||||
@ -6,8 +6,8 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.serializers import ModelSerializer
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import Event, EventAction
 | 
				
			||||||
from authentik.core.models import Token
 | 
					from authentik.core.models import Token
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TokenSerializer(ModelSerializer):
 | 
					class TokenSerializer(ModelSerializer):
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@ class UserSerializer(ModelSerializer):
 | 
				
			|||||||
class UserViewSet(ModelViewSet):
 | 
					class UserViewSet(ModelViewSet):
 | 
				
			||||||
    """User Viewset"""
 | 
					    """User Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = User.objects.all()
 | 
					    queryset = User.objects.all().exclude(pk=get_anonymous_user().pk)
 | 
				
			||||||
    serializer_class = UserSerializer
 | 
					    serializer_class = UserSerializer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_queryset(self):
 | 
					    def get_queryset(self):
 | 
				
			||||||
 | 
				
			|||||||
@ -1,24 +0,0 @@
 | 
				
			|||||||
"""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,11 +1,9 @@
 | 
				
			|||||||
"""Property Mapping Evaluator"""
 | 
					"""Property Mapping Evaluator"""
 | 
				
			||||||
from traceback import format_tb
 | 
					 | 
				
			||||||
from typing import Optional
 | 
					from typing import Optional
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
					from authentik.lib.expression.evaluator import BaseEvaluator
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -21,18 +19,3 @@ class PropertyMappingEvaluator(BaseEvaluator):
 | 
				
			|||||||
        if request:
 | 
					        if request:
 | 
				
			||||||
            self._context["request"] = request
 | 
					            self._context["request"] = request
 | 
				
			||||||
        self._context.update(**kwargs)
 | 
					        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,
 | 
					 | 
				
			||||||
            error=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()
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,56 +0,0 @@
 | 
				
			|||||||
"""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",
 | 
					        name="user-tokens-delete",
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    # Libray
 | 
					    # Libray
 | 
				
			||||||
    path("library", library.LibraryView.as_view(), name="overview"),
 | 
					    path("library/", library.LibraryView.as_view(), name="overview"),
 | 
				
			||||||
    # Impersonation
 | 
					    # Impersonation
 | 
				
			||||||
    path(
 | 
					    path(
 | 
				
			||||||
        "-/impersonation/<int:user_id>/",
 | 
					        "-/impersonation/<int:user_id>/",
 | 
				
			||||||
 | 
				
			|||||||
@ -5,12 +5,12 @@ from django.shortcuts import get_object_or_404, redirect
 | 
				
			|||||||
from django.views import View
 | 
					from django.views import View
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import Event, EventAction
 | 
				
			||||||
from authentik.core.middleware import (
 | 
					from authentik.core.middleware import (
 | 
				
			||||||
    SESSION_IMPERSONATE_ORIGINAL_USER,
 | 
					    SESSION_IMPERSONATE_ORIGINAL_USER,
 | 
				
			||||||
    SESSION_IMPERSONATE_USER,
 | 
					    SESSION_IMPERSONATE_USER,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -94,6 +94,11 @@ class TokenCreateView(
 | 
				
			|||||||
    success_url = reverse_lazy("authentik_core:user-tokens")
 | 
					    success_url = reverse_lazy("authentik_core:user-tokens")
 | 
				
			||||||
    success_message = _("Successfully created Token")
 | 
					    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:
 | 
					    def form_valid(self, form: UserTokenForm) -> HttpResponse:
 | 
				
			||||||
        form.instance.user = self.request.user
 | 
					        form.instance.user = self.request.user
 | 
				
			||||||
        form.instance.intent = TokenIntents.INTENT_API
 | 
					        form.instance.intent = TokenIntents.INTENT_API
 | 
				
			||||||
@ -107,20 +112,21 @@ class TokenUpdateView(
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    model = Token
 | 
					    model = Token
 | 
				
			||||||
    form_class = UserTokenForm
 | 
					    form_class = UserTokenForm
 | 
				
			||||||
    permission_required = "authentik_core.change_token"
 | 
					    permission_required = "authentik_core.update_token"
 | 
				
			||||||
    template_name = "generic/update.html"
 | 
					    template_name = "generic/update.html"
 | 
				
			||||||
    success_url = reverse_lazy("authentik_core:user-tokens")
 | 
					    success_url = reverse_lazy("authentik_core:user-tokens")
 | 
				
			||||||
    success_message = _("Successfully updated Token")
 | 
					    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:
 | 
					    def get_object(self) -> Token:
 | 
				
			||||||
        identifier = self.kwargs.get("identifier")
 | 
					        identifier = self.kwargs.get("identifier")
 | 
				
			||||||
        return (
 | 
					        return get_objects_for_user(
 | 
				
			||||||
            get_objects_for_user(
 | 
					            self.request.user, "authentik_core.update_token", self.model
 | 
				
			||||||
                self.request.user, self.permission_required, self.model
 | 
					        ).filter(intent=TokenIntents.INTENT_API, identifier=identifier)
 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .filter(intent=TokenIntents.INTENT_API, identifier=identifier)
 | 
					 | 
				
			||||||
            .first()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
 | 
					class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessageView):
 | 
				
			||||||
@ -132,12 +138,7 @@ class TokenDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteMessage
 | 
				
			|||||||
    success_url = reverse_lazy("authentik_core:user-tokens")
 | 
					    success_url = reverse_lazy("authentik_core:user-tokens")
 | 
				
			||||||
    success_message = _("Successfully deleted Token")
 | 
					    success_message = _("Successfully deleted Token")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_object(self) -> Token:
 | 
					    def get_context_data(self, **kwargs: Any) -> Dict[str, Any]:
 | 
				
			||||||
        identifier = self.kwargs.get("identifier")
 | 
					        kwargs = super().get_context_data(**kwargs)
 | 
				
			||||||
        return (
 | 
					        kwargs["container_template"] = "user/base.html"
 | 
				
			||||||
            get_objects_for_user(
 | 
					        return kwargs
 | 
				
			||||||
                self.request.user, self.permission_required, self.model
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .filter(intent=TokenIntents.INTENT_API, identifier=identifier)
 | 
					 | 
				
			||||||
            .first()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,15 +0,0 @@
 | 
				
			|||||||
"""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")
 | 
					 | 
				
			||||||
@ -1,41 +0,0 @@
 | 
				
			|||||||
# 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"),
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -1,42 +0,0 @@
 | 
				
			|||||||
# 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"),
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -1,42 +0,0 @@
 | 
				
			|||||||
# 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"),
 | 
					 | 
				
			||||||
                ]
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -1,86 +0,0 @@
 | 
				
			|||||||
"""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.db import models
 | 
					 | 
				
			||||||
from django.db.models.base import Model
 | 
					 | 
				
			||||||
from django.views.debug import SafeExceptionReporterFilter
 | 
					 | 
				
			||||||
from guardian.utils import get_anonymous_user
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from authentik.core.models import User
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# 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):
 | 
					 | 
				
			||||||
            value = asdict(value)
 | 
					 | 
				
			||||||
        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
 | 
					 | 
				
			||||||
@ -1,17 +1,9 @@
 | 
				
			|||||||
"""Flow API Views"""
 | 
					"""Flow API Views"""
 | 
				
			||||||
from dataclasses import dataclass
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.core.cache import cache
 | 
					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.mixins import ListModelMixin
 | 
				
			||||||
from rest_framework.request import Request
 | 
					from rest_framework.request import Request
 | 
				
			||||||
from rest_framework.response import Response
 | 
					from rest_framework.response import Response
 | 
				
			||||||
from rest_framework.serializers import (
 | 
					from rest_framework.serializers import (
 | 
				
			||||||
    CharField,
 | 
					 | 
				
			||||||
    ModelSerializer,
 | 
					    ModelSerializer,
 | 
				
			||||||
    Serializer,
 | 
					    Serializer,
 | 
				
			||||||
    SerializerMethodField,
 | 
					    SerializerMethodField,
 | 
				
			||||||
@ -48,110 +40,12 @@ 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):
 | 
					class FlowViewSet(ModelViewSet):
 | 
				
			||||||
    """Flow Viewset"""
 | 
					    """Flow Viewset"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    queryset = Flow.objects.all()
 | 
					    queryset = Flow.objects.all()
 | 
				
			||||||
    serializer_class = FlowSerializer
 | 
					    serializer_class = FlowSerializer
 | 
				
			||||||
    lookup_field = "slug"
 | 
					    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):
 | 
					class StageSerializer(ModelSerializer):
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,8 @@ from sentry_sdk.hub import Hub
 | 
				
			|||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import cleanse_dict
 | 
				
			||||||
from authentik.core.models import User
 | 
					from authentik.core.models import User
 | 
				
			||||||
from authentik.events.models import cleanse_dict
 | 
					 | 
				
			||||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
 | 
					from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
 | 
				
			||||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
 | 
					from authentik.flows.markers import ReevaluateMarker, StageMarker
 | 
				
			||||||
from authentik.flows.models import Flow, FlowStageBinding, Stage
 | 
					from authentik.flows.models import Flow, FlowStageBinding, Stage
 | 
				
			||||||
 | 
				
			|||||||
@ -1,92 +0,0 @@
 | 
				
			|||||||
"""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})
 | 
					 | 
				
			||||||
							
								
								
									
										25
									
								
								authentik/flows/tests/test_misc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								authentik/flows/tests/test_misc.py
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					"""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())
 | 
				
			||||||
@ -17,8 +17,8 @@ from django.views.decorators.clickjacking import xframe_options_sameorigin
 | 
				
			|||||||
from django.views.generic import TemplateView, View
 | 
					from django.views.generic import TemplateView, View
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import cleanse_dict
 | 
				
			||||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
 | 
					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.exceptions import EmptyFlowException, FlowNonApplicableException
 | 
				
			||||||
from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
 | 
					from authentik.flows.models import ConfigurableStage, Flow, FlowDesignation, Stage
 | 
				
			||||||
from authentik.flows.planner import (
 | 
					from authentik.flows.planner import (
 | 
				
			||||||
 | 
				
			|||||||
@ -80,15 +80,11 @@ class BaseEvaluator:
 | 
				
			|||||||
            span: Span
 | 
					            span: Span
 | 
				
			||||||
            span.set_data("expression", expression_source)
 | 
					            span.set_data("expression", expression_source)
 | 
				
			||||||
            param_keys = self._context.keys()
 | 
					            param_keys = self._context.keys()
 | 
				
			||||||
            try:
 | 
					            ast_obj = compile(
 | 
				
			||||||
                ast_obj = compile(
 | 
					                self.wrap_expression(expression_source, param_keys),
 | 
				
			||||||
                    self.wrap_expression(expression_source, param_keys),
 | 
					                self._filename,
 | 
				
			||||||
                    self._filename,
 | 
					                "exec",
 | 
				
			||||||
                    "exec",
 | 
					            )
 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            except (SyntaxError, ValueError) as exc:
 | 
					 | 
				
			||||||
                self.handle_error(exc, expression_source)
 | 
					 | 
				
			||||||
                raise exc
 | 
					 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                _locals = self._context
 | 
					                _locals = self._context
 | 
				
			||||||
                # Yes this is an exec, yes it is potentially bad. Since we limit what variables are
 | 
					                # Yes this is an exec, yes it is potentially bad. Since we limit what variables are
 | 
				
			||||||
@ -98,15 +94,10 @@ class BaseEvaluator:
 | 
				
			|||||||
                exec(ast_obj, self._globals, _locals)  # nosec # noqa
 | 
					                exec(ast_obj, self._globals, _locals)  # nosec # noqa
 | 
				
			||||||
                result = _locals["result"]
 | 
					                result = _locals["result"]
 | 
				
			||||||
            except Exception as exc:
 | 
					            except Exception as exc:
 | 
				
			||||||
                self.handle_error(exc, expression_source)
 | 
					                LOGGER.warning("Expression error", exc=exc)
 | 
				
			||||||
                raise exc
 | 
					                raise
 | 
				
			||||||
            return result
 | 
					            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:
 | 
					    def validate(self, expression: str) -> bool:
 | 
				
			||||||
        """Validate expression's syntax, raise ValidationError if Syntax is invalid"""
 | 
					        """Validate expression's syntax, raise ValidationError if Syntax is invalid"""
 | 
				
			||||||
        param_keys = self._context.keys()
 | 
					        param_keys = self._context.keys()
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,5 @@
 | 
				
			|||||||
"""Base Controller"""
 | 
					"""Base Controller"""
 | 
				
			||||||
from dataclasses import dataclass
 | 
					from typing import Dict, List
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
from structlog.testing import capture_logs
 | 
					from structlog.testing import capture_logs
 | 
				
			||||||
@ -7,26 +7,15 @@ from structlog.testing import capture_logs
 | 
				
			|||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					from authentik.lib.sentry import SentryIgnoredException
 | 
				
			||||||
from authentik.outposts.models import Outpost, OutpostServiceConnection
 | 
					from authentik.outposts.models import Outpost, OutpostServiceConnection
 | 
				
			||||||
 | 
					
 | 
				
			||||||
FIELD_MANAGER = "goauthentik.io"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ControllerException(SentryIgnoredException):
 | 
					class ControllerException(SentryIgnoredException):
 | 
				
			||||||
    """Exception raised when anything fails during controller run"""
 | 
					    """Exception raised when anything fails during controller run"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					 | 
				
			||||||
class DeploymentPort:
 | 
					 | 
				
			||||||
    """Info about deployment's single port."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    port: int
 | 
					 | 
				
			||||||
    name: str
 | 
					 | 
				
			||||||
    protocol: str
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BaseController:
 | 
					class BaseController:
 | 
				
			||||||
    """Base Outpost deployment controller"""
 | 
					    """Base Outpost deployment controller"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    deployment_ports: list[DeploymentPort]
 | 
					    deployment_ports: Dict[str, int]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    outpost: Outpost
 | 
					    outpost: Outpost
 | 
				
			||||||
    connection: OutpostServiceConnection
 | 
					    connection: OutpostServiceConnection
 | 
				
			||||||
@ -35,14 +24,14 @@ class BaseController:
 | 
				
			|||||||
        self.outpost = outpost
 | 
					        self.outpost = outpost
 | 
				
			||||||
        self.connection = connection
 | 
					        self.connection = connection
 | 
				
			||||||
        self.logger = get_logger()
 | 
					        self.logger = get_logger()
 | 
				
			||||||
        self.deployment_ports = []
 | 
					        self.deployment_ports = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=invalid-name
 | 
					    # pylint: disable=invalid-name
 | 
				
			||||||
    def up(self):
 | 
					    def up(self):
 | 
				
			||||||
        """Called by scheduled task to reconcile deployment/service/etc"""
 | 
					        """Called by scheduled task to reconcile deployment/service/etc"""
 | 
				
			||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def up_with_logs(self) -> list[str]:
 | 
					    def up_with_logs(self) -> List[str]:
 | 
				
			||||||
        """Call .up() but capture all log output and return it."""
 | 
					        """Call .up() but capture all log output and return it."""
 | 
				
			||||||
        with capture_logs() as logs:
 | 
					        with capture_logs() as logs:
 | 
				
			||||||
            self.up()
 | 
					            self.up()
 | 
				
			||||||
 | 
				
			|||||||
@ -68,10 +68,7 @@ class DockerController(BaseController):
 | 
				
			|||||||
                "image": image_name,
 | 
					                "image": image_name,
 | 
				
			||||||
                "name": f"authentik-proxy-{self.outpost.uuid.hex}",
 | 
					                "name": f"authentik-proxy-{self.outpost.uuid.hex}",
 | 
				
			||||||
                "detach": True,
 | 
					                "detach": True,
 | 
				
			||||||
                "ports": {
 | 
					                "ports": {x: x for _, x in self.deployment_ports.items()},
 | 
				
			||||||
                    f"{port.port}/{port.protocol.lower()}": port.port
 | 
					 | 
				
			||||||
                    for port in self.deployment_ports
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                "environment": self._get_env(),
 | 
					                "environment": self._get_env(),
 | 
				
			||||||
                "labels": self._get_labels(),
 | 
					                "labels": self._get_labels(),
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@ -142,10 +139,7 @@ class DockerController(BaseController):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_static_deployment(self) -> str:
 | 
					    def get_static_deployment(self) -> str:
 | 
				
			||||||
        """Generate docker-compose yaml for proxy, version 3.5"""
 | 
					        """Generate docker-compose yaml for proxy, version 3.5"""
 | 
				
			||||||
        ports = [
 | 
					        ports = [f"{x}:{x}" for _, x in self.deployment_ports.items()]
 | 
				
			||||||
            f"{port.port}:{port.port}/{port.protocol.lower()}"
 | 
					 | 
				
			||||||
            for port in self.deployment_ports
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
        image_prefix = CONFIG.y("outposts.docker_image_base")
 | 
					        image_prefix = CONFIG.y("outposts.docker_image_base")
 | 
				
			||||||
        compose = {
 | 
					        compose = {
 | 
				
			||||||
            "version": "3.5",
 | 
					            "version": "3.5",
 | 
				
			||||||
@ -160,7 +154,6 @@ class DockerController(BaseController):
 | 
				
			|||||||
                        ),
 | 
					                        ),
 | 
				
			||||||
                        "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
					                        "AUTHENTIK_TOKEN": self.outpost.token.key,
 | 
				
			||||||
                    },
 | 
					                    },
 | 
				
			||||||
                    "labels": self._get_labels(),
 | 
					 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -93,8 +93,7 @@ class KubernetesObjectReconciler(Generic[T]):
 | 
				
			|||||||
    def reconcile(self, current: T, reference: T):
 | 
					    def reconcile(self, current: T, reference: T):
 | 
				
			||||||
        """Check what operations should be done, should be raised as
 | 
					        """Check what operations should be done, should be raised as
 | 
				
			||||||
        ReconcileTrigger"""
 | 
					        ReconcileTrigger"""
 | 
				
			||||||
        if current.metadata.annotations != reference.metadata.annotations:
 | 
					        raise NotImplementedError
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, reference: T):
 | 
					    def create(self, reference: T):
 | 
				
			||||||
        """API Wrapper to create object"""
 | 
					        """API Wrapper to create object"""
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,6 @@ from kubernetes.client import (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik import __version__
 | 
					from authentik import __version__
 | 
				
			||||||
from authentik.lib.config import CONFIG
 | 
					from authentik.lib.config import CONFIG
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import (
 | 
					from authentik.outposts.controllers.k8s.base import (
 | 
				
			||||||
    KubernetesObjectReconciler,
 | 
					    KubernetesObjectReconciler,
 | 
				
			||||||
    NeedsUpdate,
 | 
					    NeedsUpdate,
 | 
				
			||||||
@ -44,7 +43,6 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
 | 
					        return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Deployment, reference: V1Deployment):
 | 
					    def reconcile(self, current: V1Deployment, reference: V1Deployment):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					 | 
				
			||||||
        if current.spec.replicas != reference.spec.replicas:
 | 
					        if current.spec.replicas != reference.spec.replicas:
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
@ -65,14 +63,8 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        """Get deployment object for outpost"""
 | 
					        """Get deployment object for outpost"""
 | 
				
			||||||
        # Generate V1ContainerPort objects
 | 
					        # Generate V1ContainerPort objects
 | 
				
			||||||
        container_ports = []
 | 
					        container_ports = []
 | 
				
			||||||
        for port in self.controller.deployment_ports:
 | 
					        for port_name, port in self.controller.deployment_ports.items():
 | 
				
			||||||
            container_ports.append(
 | 
					            container_ports.append(V1ContainerPort(container_port=port, name=port_name))
 | 
				
			||||||
                V1ContainerPort(
 | 
					 | 
				
			||||||
                    container_port=port.port,
 | 
					 | 
				
			||||||
                    name=port.name,
 | 
					 | 
				
			||||||
                    protocol=port.protocol.upper(),
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        meta = self.get_object_meta(name=self.name)
 | 
					        meta = self.get_object_meta(name=self.name)
 | 
				
			||||||
        secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
 | 
					        secret_name = f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
 | 
				
			||||||
        image_prefix = CONFIG.y("outposts.docker_image_base")
 | 
					        image_prefix = CONFIG.y("outposts.docker_image_base")
 | 
				
			||||||
@ -126,9 +118,7 @@ class DeploymentReconciler(KubernetesObjectReconciler[V1Deployment]):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, reference: V1Deployment):
 | 
					    def create(self, reference: V1Deployment):
 | 
				
			||||||
        return self.api.create_namespaced_deployment(
 | 
					        return self.api.create_namespaced_deployment(self.namespace, reference)
 | 
				
			||||||
            self.namespace, reference, field_manager=FIELD_MANAGER
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, reference: V1Deployment):
 | 
					    def delete(self, reference: V1Deployment):
 | 
				
			||||||
        return self.api.delete_namespaced_deployment(
 | 
					        return self.api.delete_namespaced_deployment(
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ from typing import TYPE_CHECKING
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from kubernetes.client import CoreV1Api, V1Secret
 | 
					from kubernetes.client import CoreV1Api, V1Secret
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import (
 | 
					from authentik.outposts.controllers.k8s.base import (
 | 
				
			||||||
    KubernetesObjectReconciler,
 | 
					    KubernetesObjectReconciler,
 | 
				
			||||||
    NeedsUpdate,
 | 
					    NeedsUpdate,
 | 
				
			||||||
@ -31,7 +30,6 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
        return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
 | 
					        return f"authentik-outpost-{self.controller.outpost.uuid.hex}-api"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Secret, reference: V1Secret):
 | 
					    def reconcile(self, current: V1Secret, reference: V1Secret):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					 | 
				
			||||||
        for key in reference.data.keys():
 | 
					        for key in reference.data.keys():
 | 
				
			||||||
            if current.data[key] != reference.data[key]:
 | 
					            if current.data[key] != reference.data[key]:
 | 
				
			||||||
                raise NeedsUpdate()
 | 
					                raise NeedsUpdate()
 | 
				
			||||||
@ -53,9 +51,7 @@ class SecretReconciler(KubernetesObjectReconciler[V1Secret]):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, reference: V1Secret):
 | 
					    def create(self, reference: V1Secret):
 | 
				
			||||||
        return self.api.create_namespaced_secret(
 | 
					        return self.api.create_namespaced_secret(self.namespace, reference)
 | 
				
			||||||
            self.namespace, reference, field_manager=FIELD_MANAGER
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, reference: V1Secret):
 | 
					    def delete(self, reference: V1Secret):
 | 
				
			||||||
        return self.api.delete_namespaced_secret(
 | 
					        return self.api.delete_namespaced_secret(
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
 | 
					from kubernetes.client import CoreV1Api, V1Service, V1ServicePort, V1ServiceSpec
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import (
 | 
					from authentik.outposts.controllers.k8s.base import (
 | 
				
			||||||
    KubernetesObjectReconciler,
 | 
					    KubernetesObjectReconciler,
 | 
				
			||||||
    NeedsUpdate,
 | 
					    NeedsUpdate,
 | 
				
			||||||
@ -26,7 +25,6 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
				
			|||||||
        return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
 | 
					        return f"authentik-outpost-{self.controller.outpost.uuid.hex}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def reconcile(self, current: V1Service, reference: V1Service):
 | 
					    def reconcile(self, current: V1Service, reference: V1Service):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					 | 
				
			||||||
        if len(current.spec.ports) != len(reference.spec.ports):
 | 
					        if len(current.spec.ports) != len(reference.spec.ports):
 | 
				
			||||||
            raise NeedsUpdate()
 | 
					            raise NeedsUpdate()
 | 
				
			||||||
        for port in reference.spec.ports:
 | 
					        for port in reference.spec.ports:
 | 
				
			||||||
@ -37,15 +35,8 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
				
			|||||||
        """Get deployment object for outpost"""
 | 
					        """Get deployment object for outpost"""
 | 
				
			||||||
        meta = self.get_object_meta(name=self.name)
 | 
					        meta = self.get_object_meta(name=self.name)
 | 
				
			||||||
        ports = []
 | 
					        ports = []
 | 
				
			||||||
        for port in self.controller.deployment_ports:
 | 
					        for port_name, port in self.controller.deployment_ports.items():
 | 
				
			||||||
            ports.append(
 | 
					            ports.append(V1ServicePort(name=port_name, port=port))
 | 
				
			||||||
                V1ServicePort(
 | 
					 | 
				
			||||||
                    name=port.name,
 | 
					 | 
				
			||||||
                    port=port.port,
 | 
					 | 
				
			||||||
                    protocol=port.protocol.upper(),
 | 
					 | 
				
			||||||
                    target_port=port.port,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
 | 
					        selector_labels = DeploymentReconciler(self.controller).get_pod_meta()
 | 
				
			||||||
        return V1Service(
 | 
					        return V1Service(
 | 
				
			||||||
            metadata=meta,
 | 
					            metadata=meta,
 | 
				
			||||||
@ -53,9 +44,7 @@ class ServiceReconciler(KubernetesObjectReconciler[V1Service]):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, reference: V1Service):
 | 
					    def create(self, reference: V1Service):
 | 
				
			||||||
        return self.api.create_namespaced_service(
 | 
					        return self.api.create_namespaced_service(self.namespace, reference)
 | 
				
			||||||
            self.namespace, reference, field_manager=FIELD_MANAGER
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, reference: V1Service):
 | 
					    def delete(self, reference: V1Service):
 | 
				
			||||||
        return self.api.delete_namespaced_service(
 | 
					        return self.api.delete_namespaced_service(
 | 
				
			||||||
 | 
				
			|||||||
@ -47,8 +47,6 @@ class PolicyEngine:
 | 
				
			|||||||
    __cached_policies: List[PolicyResult]
 | 
					    __cached_policies: List[PolicyResult]
 | 
				
			||||||
    __processes: List[PolicyProcessInfo]
 | 
					    __processes: List[PolicyProcessInfo]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    __expected_result_count: int
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
        self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
 | 
					        self, pbm: PolicyBindingModel, user: User, request: HttpRequest = None
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
@ -61,7 +59,6 @@ class PolicyEngine:
 | 
				
			|||||||
        self.__cached_policies = []
 | 
					        self.__cached_policies = []
 | 
				
			||||||
        self.__processes = []
 | 
					        self.__processes = []
 | 
				
			||||||
        self.use_cache = True
 | 
					        self.use_cache = True
 | 
				
			||||||
        self.__expected_result_count = 0
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _iter_bindings(self) -> Iterator[PolicyBinding]:
 | 
					    def _iter_bindings(self) -> Iterator[PolicyBinding]:
 | 
				
			||||||
        """Make sure all Policies are their respective classes"""
 | 
					        """Make sure all Policies are their respective classes"""
 | 
				
			||||||
@ -82,8 +79,6 @@ class PolicyEngine:
 | 
				
			|||||||
            span.set_data("pbm", self.__pbm)
 | 
					            span.set_data("pbm", self.__pbm)
 | 
				
			||||||
            span.set_data("request", self.request)
 | 
					            span.set_data("request", self.request)
 | 
				
			||||||
            for binding in self._iter_bindings():
 | 
					            for binding in self._iter_bindings():
 | 
				
			||||||
                self.__expected_result_count += 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                self._check_policy_type(binding.policy)
 | 
					                self._check_policy_type(binding.policy)
 | 
				
			||||||
                key = cache_key(binding, self.request)
 | 
					                key = cache_key(binding, self.request)
 | 
				
			||||||
                cached_policy = cache.get(key, None)
 | 
					                cached_policy = cache.get(key, None)
 | 
				
			||||||
@ -117,13 +112,10 @@ class PolicyEngine:
 | 
				
			|||||||
        process_results: List[PolicyResult] = [
 | 
					        process_results: List[PolicyResult] = [
 | 
				
			||||||
            x.result for x in self.__processes if x.result
 | 
					            x.result for x in self.__processes if x.result
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        all_results = list(process_results + self.__cached_policies)
 | 
					 | 
				
			||||||
        final_result = PolicyResult(False)
 | 
					        final_result = PolicyResult(False)
 | 
				
			||||||
        final_result.messages = []
 | 
					        final_result.messages = []
 | 
				
			||||||
        final_result.source_results = all_results
 | 
					        final_result.source_results = list(process_results + self.__cached_policies)
 | 
				
			||||||
        if len(all_results) < self.__expected_result_count:  # pragma: no cover
 | 
					        for result in process_results + self.__cached_policies:
 | 
				
			||||||
            raise AssertionError("Got less results than polices")
 | 
					 | 
				
			||||||
        for result in all_results:
 | 
					 | 
				
			||||||
            LOGGER.debug(
 | 
					            LOGGER.debug(
 | 
				
			||||||
                "P_ENG: result", passing=result.passing, messages=result.messages
 | 
					                "P_ENG: result", passing=result.passing, messages=result.messages
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,16 @@
 | 
				
			|||||||
"""authentik expression policy evaluator"""
 | 
					"""authentik expression policy evaluator"""
 | 
				
			||||||
from ipaddress import ip_address, ip_network
 | 
					from ipaddress import ip_address, ip_network
 | 
				
			||||||
from traceback import format_tb
 | 
					from typing import List
 | 
				
			||||||
from typing import TYPE_CHECKING, List, Optional
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest
 | 
					from django.http import HttpRequest
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.events.utils import model_to_dict, sanitize_dict
 | 
					 | 
				
			||||||
from authentik.flows.planner import PLAN_CONTEXT_SSO
 | 
					from authentik.flows.planner import PLAN_CONTEXT_SSO
 | 
				
			||||||
from authentik.lib.expression.evaluator import BaseEvaluator
 | 
					from authentik.lib.expression.evaluator import BaseEvaluator
 | 
				
			||||||
from authentik.lib.utils.http import get_client_ip
 | 
					from authentik.lib.utils.http import get_client_ip
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
if TYPE_CHECKING:
 | 
					 | 
				
			||||||
    from authentik.policies.expression.models import ExpressionPolicy
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PolicyEvaluator(BaseEvaluator):
 | 
					class PolicyEvaluator(BaseEvaluator):
 | 
				
			||||||
@ -23,8 +18,6 @@ class PolicyEvaluator(BaseEvaluator):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    _messages: List[str]
 | 
					    _messages: List[str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    policy: Optional["ExpressionPolicy"] = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, policy_name: str):
 | 
					    def __init__(self, policy_name: str):
 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
        self._messages = []
 | 
					        self._messages = []
 | 
				
			||||||
@ -53,30 +46,15 @@ class PolicyEvaluator(BaseEvaluator):
 | 
				
			|||||||
        self._context["ak_client_ip"] = ip_address(
 | 
					        self._context["ak_client_ip"] = ip_address(
 | 
				
			||||||
            get_client_ip(request) or "255.255.255.255"
 | 
					            get_client_ip(request) or "255.255.255.255"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self._context["http_request"] = request
 | 
					        self._context["request"] = request
 | 
				
			||||||
 | 
					 | 
				
			||||||
    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.POLICY_EXCEPTION,
 | 
					 | 
				
			||||||
            expression=expression_source,
 | 
					 | 
				
			||||||
            error=error_string,
 | 
					 | 
				
			||||||
            request=self._context["request"],
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if self.policy:
 | 
					 | 
				
			||||||
            event.context["model"] = sanitize_dict(model_to_dict(self.policy))
 | 
					 | 
				
			||||||
        if "http_request" in self._context:
 | 
					 | 
				
			||||||
            event.from_http(self._context["http_request"])
 | 
					 | 
				
			||||||
        else:
 | 
					 | 
				
			||||||
            event.set_user(self._context["request"].user)
 | 
					 | 
				
			||||||
            event.save()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def evaluate(self, expression_source: str) -> PolicyResult:
 | 
					    def evaluate(self, expression_source: str) -> PolicyResult:
 | 
				
			||||||
        """Parse and evaluate expression. Policy is expected to return a truthy object.
 | 
					        """Parse and evaluate expression. Policy is expected to return a truthy object.
 | 
				
			||||||
        Messages can be added using 'do ak_message()'."""
 | 
					        Messages can be added using 'do ak_message()'."""
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            result = super().evaluate(expression_source)
 | 
					            result = super().evaluate(expression_source)
 | 
				
			||||||
 | 
					        except (ValueError, SyntaxError) as exc:
 | 
				
			||||||
 | 
					            return PolicyResult(False, str(exc))
 | 
				
			||||||
        except Exception as exc:  # pylint: disable=broad-except
 | 
					        except Exception as exc:  # pylint: disable=broad-except
 | 
				
			||||||
            LOGGER.warning("Expression error", exc=exc)
 | 
					            LOGGER.warning("Expression error", exc=exc)
 | 
				
			||||||
            return PolicyResult(False, str(exc))
 | 
					            return PolicyResult(False, str(exc))
 | 
				
			||||||
 | 
				
			|||||||
@ -31,14 +31,11 @@ class ExpressionPolicy(Policy):
 | 
				
			|||||||
    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
					    def passes(self, request: PolicyRequest) -> PolicyResult:
 | 
				
			||||||
        """Evaluate and render expression. Returns PolicyResult(false) on error."""
 | 
					        """Evaluate and render expression. Returns PolicyResult(false) on error."""
 | 
				
			||||||
        evaluator = PolicyEvaluator(self.name)
 | 
					        evaluator = PolicyEvaluator(self.name)
 | 
				
			||||||
        evaluator.policy = self
 | 
					 | 
				
			||||||
        evaluator.set_policy_request(request)
 | 
					        evaluator.set_policy_request(request)
 | 
				
			||||||
        return evaluator.evaluate(self.expression)
 | 
					        return evaluator.evaluate(self.expression)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def save(self, *args, **kwargs):
 | 
					    def save(self, *args, **kwargs):
 | 
				
			||||||
        evaluator = PolicyEvaluator(self.name)
 | 
					        PolicyEvaluator(self.name).validate(self.expression)
 | 
				
			||||||
        evaluator.policy = self
 | 
					 | 
				
			||||||
        evaluator.validate(self.expression)
 | 
					 | 
				
			||||||
        return super().save(*args, **kwargs)
 | 
					        return super().save(*args, **kwargs)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
 | 
				
			|||||||
@ -3,9 +3,7 @@ from django.core.exceptions import ValidationError
 | 
				
			|||||||
from django.test import TestCase
 | 
					from django.test import TestCase
 | 
				
			||||||
from guardian.shortcuts import get_anonymous_user
 | 
					from guardian.shortcuts import get_anonymous_user
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.policies.expression.evaluator import PolicyEvaluator
 | 
					from authentik.policies.expression.evaluator import PolicyEvaluator
 | 
				
			||||||
from authentik.policies.expression.models import ExpressionPolicy
 | 
					 | 
				
			||||||
from authentik.policies.types import PolicyRequest
 | 
					from authentik.policies.types import PolicyRequest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -15,14 +13,6 @@ class TestEvaluator(TestCase):
 | 
				
			|||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        self.request = PolicyRequest(user=get_anonymous_user())
 | 
					        self.request = PolicyRequest(user=get_anonymous_user())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_full(self):
 | 
					 | 
				
			||||||
        """Test full with Policy instance"""
 | 
					 | 
				
			||||||
        policy = ExpressionPolicy(name="test", expression="return 'test'")
 | 
					 | 
				
			||||||
        policy.save()
 | 
					 | 
				
			||||||
        request = PolicyRequest(get_anonymous_user())
 | 
					 | 
				
			||||||
        result = policy.passes(request)
 | 
					 | 
				
			||||||
        self.assertTrue(result.passing)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_valid(self):
 | 
					    def test_valid(self):
 | 
				
			||||||
        """test simple value expression"""
 | 
					        """test simple value expression"""
 | 
				
			||||||
        template = "return True"
 | 
					        template = "return True"
 | 
				
			||||||
@ -47,12 +37,6 @@ class TestEvaluator(TestCase):
 | 
				
			|||||||
        result = evaluator.evaluate(template)
 | 
					        result = evaluator.evaluate(template)
 | 
				
			||||||
        self.assertEqual(result.passing, False)
 | 
					        self.assertEqual(result.passing, False)
 | 
				
			||||||
        self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
 | 
					        self.assertEqual(result.messages, ("invalid syntax (test, line 3)",))
 | 
				
			||||||
        self.assertTrue(
 | 
					 | 
				
			||||||
            Event.objects.filter(
 | 
					 | 
				
			||||||
                action=EventAction.POLICY_EXCEPTION,
 | 
					 | 
				
			||||||
                context__expression=template,
 | 
					 | 
				
			||||||
            ).exists()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_undefined(self):
 | 
					    def test_undefined(self):
 | 
				
			||||||
        """test undefined result"""
 | 
					        """test undefined result"""
 | 
				
			||||||
@ -62,12 +46,6 @@ class TestEvaluator(TestCase):
 | 
				
			|||||||
        result = evaluator.evaluate(template)
 | 
					        result = evaluator.evaluate(template)
 | 
				
			||||||
        self.assertEqual(result.passing, False)
 | 
					        self.assertEqual(result.passing, False)
 | 
				
			||||||
        self.assertEqual(result.messages, ("name 'foo' is not defined",))
 | 
					        self.assertEqual(result.messages, ("name 'foo' is not defined",))
 | 
				
			||||||
        self.assertTrue(
 | 
					 | 
				
			||||||
            Event.objects.filter(
 | 
					 | 
				
			||||||
                action=EventAction.POLICY_EXCEPTION,
 | 
					 | 
				
			||||||
                context__expression=template,
 | 
					 | 
				
			||||||
            ).exists()
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_validate(self):
 | 
					    def test_validate(self):
 | 
				
			||||||
        """test validate"""
 | 
					        """test validate"""
 | 
				
			||||||
 | 
				
			|||||||
@ -5,7 +5,7 @@ from django import forms
 | 
				
			|||||||
from authentik.lib.widgets import GroupedModelChoiceField
 | 
					from authentik.lib.widgets import GroupedModelChoiceField
 | 
				
			||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
					from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
				
			||||||
 | 
					
 | 
				
			||||||
GENERAL_FIELDS = ["name", "execution_logging"]
 | 
					GENERAL_FIELDS = ["name"]
 | 
				
			||||||
GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
 | 
					GENERAL_SERIALIZER_FIELDS = ["pk", "name"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +0,0 @@
 | 
				
			|||||||
# Generated by Django 3.1.4 on 2020-12-15 09:41
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ("authentik_policies", "0003_auto_20200908_1542"),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name="policy",
 | 
					 | 
				
			||||||
            name="execution_logging",
 | 
					 | 
				
			||||||
            field=models.BooleanField(
 | 
					 | 
				
			||||||
                default=False,
 | 
					 | 
				
			||||||
                help_text="When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged.",
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -81,16 +81,6 @@ class Policy(SerializerModel, CreatedUpdatedModel):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    name = models.TextField(blank=True, null=True)
 | 
					    name = models.TextField(blank=True, null=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    execution_logging = models.BooleanField(
 | 
					 | 
				
			||||||
        default=False,
 | 
					 | 
				
			||||||
        help_text=_(
 | 
					 | 
				
			||||||
            (
 | 
					 | 
				
			||||||
                "When this option is enabled, all executions of this policy will be logged. "
 | 
					 | 
				
			||||||
                "By default, only execution errors are logged."
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    objects = InheritanceAutoManager()
 | 
					    objects = InheritanceAutoManager()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
 | 
				
			|||||||
@ -8,7 +8,6 @@ from sentry_sdk.hub import Hub
 | 
				
			|||||||
from sentry_sdk.tracing import Span
 | 
					from sentry_sdk.tracing import Span
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.policies.exceptions import PolicyException
 | 
					from authentik.policies.exceptions import PolicyException
 | 
				
			||||||
from authentik.policies.models import PolicyBinding
 | 
					from authentik.policies.models import PolicyBinding
 | 
				
			||||||
from authentik.policies.types import PolicyRequest, PolicyResult
 | 
					from authentik.policies.types import PolicyRequest, PolicyResult
 | 
				
			||||||
@ -49,48 +48,40 @@ class PolicyProcess(Process):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def execute(self) -> PolicyResult:
 | 
					    def execute(self) -> PolicyResult:
 | 
				
			||||||
        """Run actual policy, returns result"""
 | 
					        """Run actual policy, returns result"""
 | 
				
			||||||
        LOGGER.debug(
 | 
					 | 
				
			||||||
            "P_ENG(proc): Running policy",
 | 
					 | 
				
			||||||
            policy=self.binding.policy,
 | 
					 | 
				
			||||||
            user=self.request.user,
 | 
					 | 
				
			||||||
            process="PolicyProcess",
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            policy_result = self.binding.policy.passes(self.request)
 | 
					 | 
				
			||||||
            if self.binding.policy.execution_logging:
 | 
					 | 
				
			||||||
                event = Event.new(
 | 
					 | 
				
			||||||
                    EventAction.POLICY_EXECUTION,
 | 
					 | 
				
			||||||
                    request=self.request,
 | 
					 | 
				
			||||||
                    result=policy_result,
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                event.set_user(self.request.user)
 | 
					 | 
				
			||||||
                event.save()
 | 
					 | 
				
			||||||
        except PolicyException as exc:
 | 
					 | 
				
			||||||
            LOGGER.debug("P_ENG(proc): error", exc=exc)
 | 
					 | 
				
			||||||
            policy_result = PolicyResult(False, str(exc))
 | 
					 | 
				
			||||||
        policy_result.source_policy = self.binding.policy
 | 
					 | 
				
			||||||
        # Invert result if policy.negate is set
 | 
					 | 
				
			||||||
        if self.binding.negate:
 | 
					 | 
				
			||||||
            policy_result.passing = not policy_result.passing
 | 
					 | 
				
			||||||
        LOGGER.debug(
 | 
					 | 
				
			||||||
            "P_ENG(proc): Finished",
 | 
					 | 
				
			||||||
            policy=self.binding.policy,
 | 
					 | 
				
			||||||
            result=policy_result,
 | 
					 | 
				
			||||||
            process="PolicyProcess",
 | 
					 | 
				
			||||||
            passing=policy_result.passing,
 | 
					 | 
				
			||||||
            user=self.request.user,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        key = cache_key(self.binding, self.request)
 | 
					 | 
				
			||||||
        cache.set(key, policy_result)
 | 
					 | 
				
			||||||
        LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
 | 
					 | 
				
			||||||
        return policy_result
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def run(self):  # pragma: no cover
 | 
					 | 
				
			||||||
        """Task wrapper to run policy checking"""
 | 
					 | 
				
			||||||
        with Hub.current.start_span(
 | 
					        with Hub.current.start_span(
 | 
				
			||||||
            op="policy.process.execute",
 | 
					            op="policy.process.execute",
 | 
				
			||||||
        ) as span:
 | 
					        ) as span:
 | 
				
			||||||
            span: Span
 | 
					            span: Span
 | 
				
			||||||
            span.set_data("policy", self.binding.policy)
 | 
					            span.set_data("policy", self.binding.policy)
 | 
				
			||||||
            span.set_data("request", self.request)
 | 
					            span.set_data("request", self.request)
 | 
				
			||||||
            self.connection.send(self.execute())
 | 
					            LOGGER.debug(
 | 
				
			||||||
 | 
					                "P_ENG(proc): Running policy",
 | 
				
			||||||
 | 
					                policy=self.binding.policy,
 | 
				
			||||||
 | 
					                user=self.request.user,
 | 
				
			||||||
 | 
					                process="PolicyProcess",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            try:
 | 
				
			||||||
 | 
					                policy_result = self.binding.policy.passes(self.request)
 | 
				
			||||||
 | 
					            except PolicyException as exc:
 | 
				
			||||||
 | 
					                LOGGER.debug("P_ENG(proc): error", exc=exc)
 | 
				
			||||||
 | 
					                policy_result = PolicyResult(False, str(exc))
 | 
				
			||||||
 | 
					            policy_result.source_policy = self.binding.policy
 | 
				
			||||||
 | 
					            # Invert result if policy.negate is set
 | 
				
			||||||
 | 
					            if self.binding.negate:
 | 
				
			||||||
 | 
					                policy_result.passing = not policy_result.passing
 | 
				
			||||||
 | 
					            LOGGER.debug(
 | 
				
			||||||
 | 
					                "P_ENG(proc): Finished",
 | 
				
			||||||
 | 
					                policy=self.binding.policy,
 | 
				
			||||||
 | 
					                result=policy_result,
 | 
				
			||||||
 | 
					                process="PolicyProcess",
 | 
				
			||||||
 | 
					                passing=policy_result.passing,
 | 
				
			||||||
 | 
					                user=self.request.user,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            key = cache_key(self.binding, self.request)
 | 
				
			||||||
 | 
					            cache.set(key, policy_result)
 | 
				
			||||||
 | 
					            LOGGER.debug("P_ENG(proc): Cached policy evaluation", key=key)
 | 
				
			||||||
 | 
					            return policy_result
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def run(self):
 | 
				
			||||||
 | 
					        """Task wrapper to run policy checking"""
 | 
				
			||||||
 | 
					        self.connection.send(self.execute())
 | 
				
			||||||
 | 
				
			|||||||
@ -7,14 +7,13 @@ from authentik.policies.dummy.models import DummyPolicy
 | 
				
			|||||||
from authentik.policies.engine import PolicyEngine
 | 
					from authentik.policies.engine import PolicyEngine
 | 
				
			||||||
from authentik.policies.expression.models import ExpressionPolicy
 | 
					from authentik.policies.expression.models import ExpressionPolicy
 | 
				
			||||||
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
					from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
 | 
				
			||||||
from authentik.policies.tests.test_process import clear_policy_cache
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class TestPolicyEngine(TestCase):
 | 
					class TestPolicyEngine(TestCase):
 | 
				
			||||||
    """PolicyEngine tests"""
 | 
					    """PolicyEngine tests"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def setUp(self):
 | 
					    def setUp(self):
 | 
				
			||||||
        clear_policy_cache()
 | 
					        cache.clear()
 | 
				
			||||||
        self.user = User.objects.create_user(username="policyuser")
 | 
					        self.user = User.objects.create_user(username="policyuser")
 | 
				
			||||||
        self.policy_false = DummyPolicy.objects.create(
 | 
					        self.policy_false = DummyPolicy.objects.create(
 | 
				
			||||||
            result=False, wait_min=0, wait_max=1
 | 
					            result=False, wait_min=0, wait_max=1
 | 
				
			||||||
@ -35,15 +34,6 @@ class TestPolicyEngine(TestCase):
 | 
				
			|||||||
        self.assertEqual(result.passing, True)
 | 
					        self.assertEqual(result.passing, True)
 | 
				
			||||||
        self.assertEqual(result.messages, ())
 | 
					        self.assertEqual(result.messages, ())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def test_engine_simple(self):
 | 
					 | 
				
			||||||
        """Ensure simplest use-case"""
 | 
					 | 
				
			||||||
        pbm = PolicyBindingModel.objects.create()
 | 
					 | 
				
			||||||
        PolicyBinding.objects.create(target=pbm, policy=self.policy_true, order=0)
 | 
					 | 
				
			||||||
        engine = PolicyEngine(pbm, self.user)
 | 
					 | 
				
			||||||
        result = engine.build().result
 | 
					 | 
				
			||||||
        self.assertEqual(result.passing, True)
 | 
					 | 
				
			||||||
        self.assertEqual(result.messages, ("dummy",))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_engine(self):
 | 
					    def test_engine(self):
 | 
				
			||||||
        """Ensure all policies passes (Mix of false and true -> false)"""
 | 
					        """Ensure all policies passes (Mix of false and true -> false)"""
 | 
				
			||||||
        pbm = PolicyBindingModel.objects.create()
 | 
					        pbm = PolicyBindingModel.objects.create()
 | 
				
			||||||
@ -85,18 +75,10 @@ class TestPolicyEngine(TestCase):
 | 
				
			|||||||
    def test_engine_cache(self):
 | 
					    def test_engine_cache(self):
 | 
				
			||||||
        """Ensure empty policy list passes"""
 | 
					        """Ensure empty policy list passes"""
 | 
				
			||||||
        pbm = PolicyBindingModel.objects.create()
 | 
					        pbm = PolicyBindingModel.objects.create()
 | 
				
			||||||
        binding = PolicyBinding.objects.create(
 | 
					        PolicyBinding.objects.create(target=pbm, policy=self.policy_false, order=0)
 | 
				
			||||||
            target=pbm, policy=self.policy_false, order=0
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        engine = PolicyEngine(pbm, self.user)
 | 
					        engine = PolicyEngine(pbm, self.user)
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(len(cache.keys("policy_*")), 0)
 | 
				
			||||||
            len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 0
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        self.assertEqual(engine.build().passing, False)
 | 
					        self.assertEqual(engine.build().passing, False)
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(len(cache.keys("policy_*")), 1)
 | 
				
			||||||
            len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        self.assertEqual(engine.build().passing, False)
 | 
					        self.assertEqual(engine.build().passing, False)
 | 
				
			||||||
        self.assertEqual(
 | 
					        self.assertEqual(len(cache.keys("policy_*")), 1)
 | 
				
			||||||
            len(cache.keys(f"policy_{binding.policy_binding_uuid.hex}*")), 1
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,105 +0,0 @@
 | 
				
			|||||||
"""policy process tests"""
 | 
					 | 
				
			||||||
from django.core.cache import cache
 | 
					 | 
				
			||||||
from django.test import TestCase
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from authentik.core.models import User
 | 
					 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.policies.dummy.models import DummyPolicy
 | 
					 | 
				
			||||||
from authentik.policies.expression.models import ExpressionPolicy
 | 
					 | 
				
			||||||
from authentik.policies.models import Policy, PolicyBinding
 | 
					 | 
				
			||||||
from authentik.policies.process import PolicyProcess
 | 
					 | 
				
			||||||
from authentik.policies.types import PolicyRequest
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def clear_policy_cache():
 | 
					 | 
				
			||||||
    """Ensure no policy-related keys are stil cached"""
 | 
					 | 
				
			||||||
    keys = cache.keys("policy_*")
 | 
					 | 
				
			||||||
    cache.delete(keys)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class TestPolicyProcess(TestCase):
 | 
					 | 
				
			||||||
    """Policy Process tests"""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def setUp(self):
 | 
					 | 
				
			||||||
        clear_policy_cache()
 | 
					 | 
				
			||||||
        self.user = User.objects.create_user(username="policyuser")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_invalid(self):
 | 
					 | 
				
			||||||
        """Test Process with invalid arguments"""
 | 
					 | 
				
			||||||
        policy = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1)
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					 | 
				
			||||||
        with self.assertRaises(ValueError):
 | 
					 | 
				
			||||||
            PolicyProcess(binding, None, None)  # type: ignore
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_true(self):
 | 
					 | 
				
			||||||
        """Test policy execution"""
 | 
					 | 
				
			||||||
        policy = DummyPolicy.objects.create(result=True, wait_min=0, wait_max=1)
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					 | 
				
			||||||
        self.assertEqual(response.passing, True)
 | 
					 | 
				
			||||||
        self.assertEqual(response.messages, ("dummy",))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_false(self):
 | 
					 | 
				
			||||||
        """Test policy execution"""
 | 
					 | 
				
			||||||
        policy = DummyPolicy.objects.create(result=False, wait_min=0, wait_max=1)
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					 | 
				
			||||||
        self.assertEqual(response.passing, False)
 | 
					 | 
				
			||||||
        self.assertEqual(response.messages, ("dummy",))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_negate(self):
 | 
					 | 
				
			||||||
        """Test policy execution"""
 | 
					 | 
				
			||||||
        policy = DummyPolicy.objects.create(result=False, wait_min=0, wait_max=1)
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy, negate=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					 | 
				
			||||||
        self.assertEqual(response.passing, True)
 | 
					 | 
				
			||||||
        self.assertEqual(response.messages, ("dummy",))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_exception(self):
 | 
					 | 
				
			||||||
        """Test policy execution"""
 | 
					 | 
				
			||||||
        policy = Policy.objects.create()
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					 | 
				
			||||||
        self.assertEqual(response.passing, False)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_execution_logging(self):
 | 
					 | 
				
			||||||
        """Test policy execution creates event"""
 | 
					 | 
				
			||||||
        policy = DummyPolicy.objects.create(
 | 
					 | 
				
			||||||
            result=False, wait_min=0, wait_max=1, execution_logging=True
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					 | 
				
			||||||
        self.assertEqual(response.passing, False)
 | 
					 | 
				
			||||||
        self.assertEqual(response.messages, ("dummy",))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        events = Event.objects.filter(
 | 
					 | 
				
			||||||
            action=EventAction.POLICY_EXECUTION,
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        self.assertTrue(events.exists())
 | 
					 | 
				
			||||||
        self.assertEqual(len(events), 1)
 | 
					 | 
				
			||||||
        event = events.first()
 | 
					 | 
				
			||||||
        self.assertEqual(event.context["result"]["passing"], False)
 | 
					 | 
				
			||||||
        self.assertEqual(event.context["result"]["messages"], ["dummy"])
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def test_raises(self):
 | 
					 | 
				
			||||||
        """Test policy that raises error"""
 | 
					 | 
				
			||||||
        policy_raises = ExpressionPolicy.objects.create(
 | 
					 | 
				
			||||||
            name="raises", expression="{{ 0/0 }}"
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        binding = PolicyBinding(policy=policy_raises)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        request = PolicyRequest(self.user)
 | 
					 | 
				
			||||||
        response = PolicyProcess(binding, request, None).execute()
 | 
					 | 
				
			||||||
        self.assertEqual(response.passing, False)
 | 
					 | 
				
			||||||
        self.assertEqual(response.messages, ("division by zero",))
 | 
					 | 
				
			||||||
        # self.assert
 | 
					 | 
				
			||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
"""policy structures"""
 | 
					"""policy structures"""
 | 
				
			||||||
from __future__ import annotations
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dataclasses import dataclass
 | 
					 | 
				
			||||||
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
 | 
					from typing import TYPE_CHECKING, Dict, List, Optional, Tuple
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.db.models import Model
 | 
					from django.db.models import Model
 | 
				
			||||||
@ -12,7 +11,6 @@ if TYPE_CHECKING:
 | 
				
			|||||||
    from authentik.policies.models import Policy
 | 
					    from authentik.policies.models import Policy
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					 | 
				
			||||||
class PolicyRequest:
 | 
					class PolicyRequest:
 | 
				
			||||||
    """Data-class to hold policy request data"""
 | 
					    """Data-class to hold policy request data"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -22,7 +20,6 @@ class PolicyRequest:
 | 
				
			|||||||
    context: Dict[str, str]
 | 
					    context: Dict[str, str]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, user: User):
 | 
					    def __init__(self, user: User):
 | 
				
			||||||
        super().__init__()
 | 
					 | 
				
			||||||
        self.user = user
 | 
					        self.user = user
 | 
				
			||||||
        self.http_request = None
 | 
					        self.http_request = None
 | 
				
			||||||
        self.obj = None
 | 
					        self.obj = None
 | 
				
			||||||
@ -32,7 +29,6 @@ class PolicyRequest:
 | 
				
			|||||||
        return f"<PolicyRequest user={self.user}>"
 | 
					        return f"<PolicyRequest user={self.user}>"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					 | 
				
			||||||
class PolicyResult:
 | 
					class PolicyResult:
 | 
				
			||||||
    """Small data-class to hold policy results"""
 | 
					    """Small data-class to hold policy results"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,7 +39,6 @@ class PolicyResult:
 | 
				
			|||||||
    source_results: Optional[List["PolicyResult"]]
 | 
					    source_results: Optional[List["PolicyResult"]]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, passing: bool, *messages: str):
 | 
					    def __init__(self, passing: bool, *messages: str):
 | 
				
			||||||
        super().__init__()
 | 
					 | 
				
			||||||
        self.passing = passing
 | 
					        self.passing = passing
 | 
				
			||||||
        self.messages = messages
 | 
					        self.messages = messages
 | 
				
			||||||
        self.source_policy = None
 | 
					        self.source_policy = None
 | 
				
			||||||
@ -54,5 +49,5 @@ class PolicyResult:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        if self.messages:
 | 
					        if self.messages:
 | 
				
			||||||
            return f"<PolicyResult passing={self.passing} messages={self.messages}>"
 | 
					            return f"PolicyResult passing={self.passing} messages={self.messages}"
 | 
				
			||||||
        return f"<PolicyResult passing={self.passing}>"
 | 
					        return f"PolicyResult passing={self.passing}"
 | 
				
			||||||
 | 
				
			|||||||
@ -11,7 +11,6 @@ from structlog import get_logger
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import Application, Provider, User
 | 
					from authentik.core.models import Application, Provider, User
 | 
				
			||||||
from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
 | 
					from authentik.flows.views import SESSION_KEY_APPLICATION_PRE
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					 | 
				
			||||||
from authentik.policies.engine import PolicyEngine
 | 
					from authentik.policies.engine import PolicyEngine
 | 
				
			||||||
from authentik.policies.http import AccessDeniedResponse
 | 
					from authentik.policies.http import AccessDeniedResponse
 | 
				
			||||||
from authentik.policies.types import PolicyResult
 | 
					from authentik.policies.types import PolicyResult
 | 
				
			||||||
@ -19,17 +18,6 @@ from authentik.policies.types import PolicyResult
 | 
				
			|||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class RequestValidationError(SentryIgnoredException):
 | 
					 | 
				
			||||||
    """Error raised in pre_permission_check, when a request is invalid."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    response: Optional[HttpResponse]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(self, response: Optional[HttpResponse] = None):
 | 
					 | 
				
			||||||
        super().__init__()
 | 
					 | 
				
			||||||
        if response:
 | 
					 | 
				
			||||||
            self.response = response
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class BaseMixin:
 | 
					class BaseMixin:
 | 
				
			||||||
    """Base Mixin class, used to annotate View Member variables"""
 | 
					    """Base Mixin class, used to annotate View Member variables"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -43,10 +31,6 @@ class PolicyAccessView(AccessMixin, View):
 | 
				
			|||||||
    provider: Provider
 | 
					    provider: Provider
 | 
				
			||||||
    application: Application
 | 
					    application: Application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pre_permission_check(self):
 | 
					 | 
				
			||||||
        """Optionally hook in before permission check to check if a request is valid.
 | 
					 | 
				
			||||||
        Can raise `RequestValidationError` to return a response."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def resolve_provider_application(self):
 | 
					    def resolve_provider_application(self):
 | 
				
			||||||
        """Resolve self.provider and self.application. *.DoesNotExist Exceptions cause a normal
 | 
					        """Resolve self.provider and self.application. *.DoesNotExist Exceptions cause a normal
 | 
				
			||||||
        AccessDenied view to be shown. An Http404 exception
 | 
					        AccessDenied view to be shown. An Http404 exception
 | 
				
			||||||
@ -54,12 +38,6 @@ class PolicyAccessView(AccessMixin, View):
 | 
				
			|||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
 | 
					    def dispatch(self, request: HttpRequest, *args: Any, **kwargs: Any) -> HttpResponse:
 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            self.pre_permission_check()
 | 
					 | 
				
			||||||
        except RequestValidationError as exc:
 | 
					 | 
				
			||||||
            if exc.response:
 | 
					 | 
				
			||||||
                return exc.response
 | 
					 | 
				
			||||||
            return self.handle_no_permission()
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self.resolve_provider_application()
 | 
					            self.resolve_provider_application()
 | 
				
			||||||
        except (Application.DoesNotExist, Provider.DoesNotExist):
 | 
					        except (Application.DoesNotExist, Provider.DoesNotExist):
 | 
				
			||||||
@ -104,7 +82,7 @@ class PolicyAccessView(AccessMixin, View):
 | 
				
			|||||||
        policy_engine.build()
 | 
					        policy_engine.build()
 | 
				
			||||||
        result = policy_engine.result
 | 
					        result = policy_engine.result
 | 
				
			||||||
        LOGGER.debug(
 | 
					        LOGGER.debug(
 | 
				
			||||||
            "PolicyAccessView user_has_access",
 | 
					            "AccessMixin user_has_access",
 | 
				
			||||||
            user=user,
 | 
					            user=user,
 | 
				
			||||||
            app=self.application,
 | 
					            app=self.application,
 | 
				
			||||||
            result=result,
 | 
					            result=result,
 | 
				
			||||||
 | 
				
			|||||||
@ -2,11 +2,10 @@
 | 
				
			|||||||
from rest_framework.serializers import ModelSerializer
 | 
					from rest_framework.serializers import ModelSerializer
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.api.utils import MetaNameSerializer
 | 
					 | 
				
			||||||
from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
 | 
					from authentik.providers.oauth2.models import OAuth2Provider, ScopeMapping
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class OAuth2ProviderSerializer(ModelSerializer, MetaNameSerializer):
 | 
					class OAuth2ProviderSerializer(ModelSerializer):
 | 
				
			||||||
    """OAuth2Provider Serializer"""
 | 
					    """OAuth2Provider Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
@ -20,15 +19,12 @@ class OAuth2ProviderSerializer(ModelSerializer, MetaNameSerializer):
 | 
				
			|||||||
            "client_id",
 | 
					            "client_id",
 | 
				
			||||||
            "client_secret",
 | 
					            "client_secret",
 | 
				
			||||||
            "token_validity",
 | 
					            "token_validity",
 | 
				
			||||||
            "include_claims_in_id_token",
 | 
					            "response_type",
 | 
				
			||||||
            "jwt_alg",
 | 
					            "jwt_alg",
 | 
				
			||||||
            "rsa_key",
 | 
					            "rsa_key",
 | 
				
			||||||
            "redirect_uris",
 | 
					            "redirect_uris",
 | 
				
			||||||
            "sub_mode",
 | 
					            "sub_mode",
 | 
				
			||||||
            "property_mappings",
 | 
					            "property_mappings",
 | 
				
			||||||
            "issuer_mode",
 | 
					 | 
				
			||||||
            "verbose_name",
 | 
					 | 
				
			||||||
            "verbose_name_plural",
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ GRANT_TYPE_AUTHORIZATION_CODE = "authorization_code"
 | 
				
			|||||||
GRANT_TYPE_REFRESH_TOKEN = "refresh_token"  # nosec
 | 
					GRANT_TYPE_REFRESH_TOKEN = "refresh_token"  # nosec
 | 
				
			||||||
PROMPT_NONE = "none"
 | 
					PROMPT_NONE = "none"
 | 
				
			||||||
PROMPT_CONSNET = "consent"
 | 
					PROMPT_CONSNET = "consent"
 | 
				
			||||||
PROMPT_LOGIN = "login"
 | 
					 | 
				
			||||||
SCOPE_OPENID = "openid"
 | 
					SCOPE_OPENID = "openid"
 | 
				
			||||||
SCOPE_OPENID_PROFILE = "profile"
 | 
					SCOPE_OPENID_PROFILE = "profile"
 | 
				
			||||||
SCOPE_OPENID_EMAIL = "email"
 | 
					SCOPE_OPENID_EMAIL = "email"
 | 
				
			||||||
@ -17,5 +16,3 @@ SCOPE_GITHUB_USER_READ = "read:user"
 | 
				
			|||||||
SCOPE_GITHUB_USER_EMAIL = "user:email"
 | 
					SCOPE_GITHUB_USER_EMAIL = "user:email"
 | 
				
			||||||
# Read info about teams
 | 
					# Read info about teams
 | 
				
			||||||
SCOPE_GITHUB_ORG_READ = "read:org"
 | 
					SCOPE_GITHUB_ORG_READ = "read:org"
 | 
				
			||||||
 | 
					 | 
				
			||||||
ACR_AUTHENTIK_DEFAULT = "goauthentik.io/providers/oauth2/default"
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,8 @@
 | 
				
			|||||||
"""OAuth errors"""
 | 
					"""OAuth errors"""
 | 
				
			||||||
from urllib.parse import quote
 | 
					from urllib.parse import quote
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.lib.sentry import SentryIgnoredException
 | 
					 | 
				
			||||||
from authentik.providers.oauth2.models import GrantTypes
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class OAuth2Error(Exception):
 | 
				
			||||||
class OAuth2Error(SentryIgnoredException):
 | 
					 | 
				
			||||||
    """Base class for all OAuth2 Errors"""
 | 
					    """Base class for all OAuth2 Errors"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    error: str
 | 
					    error: str
 | 
				
			||||||
@ -99,34 +96,27 @@ class AuthorizeError(OAuth2Error):
 | 
				
			|||||||
        "the registration parameter",
 | 
					        "the registration parameter",
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(self, redirect_uri, error, grant_type):
 | 
				
			||||||
        self,
 | 
					 | 
				
			||||||
        redirect_uri: str,
 | 
					 | 
				
			||||||
        error: str,
 | 
					 | 
				
			||||||
        grant_type: str,
 | 
					 | 
				
			||||||
        state: str,
 | 
					 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
        super().__init__()
 | 
					        super().__init__()
 | 
				
			||||||
        self.error = error
 | 
					        self.error = error
 | 
				
			||||||
        self.description = self._errors[error]
 | 
					        self.description = self._errors[error]
 | 
				
			||||||
        self.redirect_uri = redirect_uri
 | 
					        self.redirect_uri = redirect_uri
 | 
				
			||||||
        self.grant_type = grant_type
 | 
					        self.grant_type = grant_type
 | 
				
			||||||
        self.state = state
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_uri(self) -> str:
 | 
					    def create_uri(self, redirect_uri: str, state: str) -> str:
 | 
				
			||||||
        """Get a redirect URI with the error message"""
 | 
					        """Get a redirect URI with the error message"""
 | 
				
			||||||
        description = quote(str(self.description))
 | 
					        description = quote(str(self.description))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # See:
 | 
					        # See:
 | 
				
			||||||
        # http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
 | 
					        # http://openid.net/specs/openid-connect-core-1_0.html#ImplicitAuthError
 | 
				
			||||||
        hash_or_question = "#" if self.grant_type == GrantTypes.IMPLICIT else "?"
 | 
					        hash_or_question = "#" if self.grant_type == "implicit" else "?"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        uri = "{0}{1}error={2}&error_description={3}".format(
 | 
					        uri = "{0}{1}error={2}&error_description={3}".format(
 | 
				
			||||||
            self.redirect_uri, hash_or_question, self.error, description
 | 
					            redirect_uri, hash_or_question, self.error, description
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Add state if present.
 | 
					        # Add state if present.
 | 
				
			||||||
        uri = uri + ("&state={0}".format(self.state) if self.state else "")
 | 
					        uri = uri + ("&state={0}".format(state) if state else "")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return uri
 | 
					        return uri
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -53,14 +53,13 @@ class OAuth2ProviderForm(forms.ModelForm):
 | 
				
			|||||||
            "client_type",
 | 
					            "client_type",
 | 
				
			||||||
            "client_id",
 | 
					            "client_id",
 | 
				
			||||||
            "client_secret",
 | 
					            "client_secret",
 | 
				
			||||||
 | 
					            "response_type",
 | 
				
			||||||
            "token_validity",
 | 
					            "token_validity",
 | 
				
			||||||
            "jwt_alg",
 | 
					            "jwt_alg",
 | 
				
			||||||
            "property_mappings",
 | 
					 | 
				
			||||||
            "rsa_key",
 | 
					            "rsa_key",
 | 
				
			||||||
            "redirect_uris",
 | 
					            "redirect_uris",
 | 
				
			||||||
            "sub_mode",
 | 
					            "sub_mode",
 | 
				
			||||||
            "include_claims_in_id_token",
 | 
					            "property_mappings",
 | 
				
			||||||
            "issuer_mode",
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
        widgets = {
 | 
					        widgets = {
 | 
				
			||||||
            "name": forms.TextInput(),
 | 
					            "name": forms.TextInput(),
 | 
				
			||||||
 | 
				
			|||||||
@ -27,6 +27,10 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            field=models.TextField(
 | 
					            field=models.TextField(
 | 
				
			||||||
                choices=[
 | 
					                choices=[
 | 
				
			||||||
                    ("code", "code (Authorization Code Flow)"),
 | 
					                    ("code", "code (Authorization Code Flow)"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "code_adfs",
 | 
				
			||||||
 | 
					                        "code (ADFS Compatibility Mode, sends id_token as access_token)",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                    ("id_token", "id_token (Implicit Flow)"),
 | 
					                    ("id_token", "id_token (Implicit Flow)"),
 | 
				
			||||||
                    ("id_token token", "id_token token (Implicit Flow)"),
 | 
					                    ("id_token token", "id_token token (Implicit Flow)"),
 | 
				
			||||||
                    ("code token", "code token (Hybrid Flow)"),
 | 
					                    ("code token", "code token (Hybrid Flow)"),
 | 
				
			||||||
 | 
				
			|||||||
@ -19,6 +19,10 @@ class Migration(migrations.Migration):
 | 
				
			|||||||
            field=models.TextField(
 | 
					            field=models.TextField(
 | 
				
			||||||
                choices=[
 | 
					                choices=[
 | 
				
			||||||
                    ("code", "code (Authorization Code Flow)"),
 | 
					                    ("code", "code (Authorization Code Flow)"),
 | 
				
			||||||
 | 
					                    (
 | 
				
			||||||
 | 
					                        "code#adfs",
 | 
				
			||||||
 | 
					                        "code (ADFS Compatibility Mode, sends id_token as access_token)",
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                    ("id_token", "id_token (Implicit Flow)"),
 | 
					                    ("id_token", "id_token (Implicit Flow)"),
 | 
				
			||||||
                    ("id_token token", "id_token token (Implicit Flow)"),
 | 
					                    ("id_token token", "id_token token (Implicit Flow)"),
 | 
				
			||||||
                    ("code token", "code token (Hybrid Flow)"),
 | 
					                    ("code token", "code token (Hybrid Flow)"),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,28 +0,0 @@
 | 
				
			|||||||
# Generated by Django 3.1.4 on 2020-12-27 13:54
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ("authentik_providers_oauth2", "0007_auto_20201016_1107"),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AddField(
 | 
					 | 
				
			||||||
            model_name="oauth2provider",
 | 
					 | 
				
			||||||
            name="issuer_mode",
 | 
					 | 
				
			||||||
            field=models.TextField(
 | 
					 | 
				
			||||||
                choices=[
 | 
					 | 
				
			||||||
                    ("global", "Same identifier is used for all providers"),
 | 
					 | 
				
			||||||
                    (
 | 
					 | 
				
			||||||
                        "per_provider",
 | 
					 | 
				
			||||||
                        "Each provider has a different issuer, based on the application slug.",
 | 
					 | 
				
			||||||
                    ),
 | 
					 | 
				
			||||||
                ],
 | 
					 | 
				
			||||||
                default="per_provider",
 | 
					 | 
				
			||||||
                help_text="Configure how the issuer field of the ID Token should be filled.",
 | 
					 | 
				
			||||||
            ),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -1,17 +0,0 @@
 | 
				
			|||||||
# Generated by Django 3.1.4 on 2020-12-27 16:32
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ("authentik_providers_oauth2", "0008_oauth2provider_issuer_mode"),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.RemoveField(
 | 
					 | 
				
			||||||
            model_name="oauth2provider",
 | 
					 | 
				
			||||||
            name="response_type",
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -1,18 +0,0 @@
 | 
				
			|||||||
# Generated by Django 3.1.4 on 2020-12-27 18:04
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
from django.db import migrations, models
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Migration(migrations.Migration):
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    dependencies = [
 | 
					 | 
				
			||||||
        ("authentik_providers_oauth2", "0009_remove_oauth2provider_response_type"),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    operations = [
 | 
					 | 
				
			||||||
        migrations.AlterField(
 | 
					 | 
				
			||||||
            model_name="refreshtoken",
 | 
					 | 
				
			||||||
            name="access_token",
 | 
					 | 
				
			||||||
            field=models.TextField(verbose_name="Access Token"),
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    ]
 | 
					 | 
				
			||||||
@ -9,7 +9,6 @@ from typing import Any, Dict, List, Optional, Type
 | 
				
			|||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dacite import from_dict
 | 
					 | 
				
			||||||
from django.conf import settings
 | 
					from django.conf import settings
 | 
				
			||||||
from django.db import models
 | 
					from django.db import models
 | 
				
			||||||
from django.forms import ModelForm
 | 
					from django.forms import ModelForm
 | 
				
			||||||
@ -23,12 +22,9 @@ from rest_framework.serializers import Serializer
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
 | 
					from authentik.core.models import ExpiringModel, PropertyMapping, Provider, User
 | 
				
			||||||
from authentik.crypto.models import CertificateKeyPair
 | 
					from authentik.crypto.models import CertificateKeyPair
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.events.utils import get_user
 | 
					 | 
				
			||||||
from authentik.lib.utils.template import render_to_string
 | 
					from authentik.lib.utils.template import render_to_string
 | 
				
			||||||
from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
 | 
					from authentik.lib.utils.time import timedelta_from_string, timedelta_string_validator
 | 
				
			||||||
from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
 | 
					from authentik.providers.oauth2.apps import AuthentikProviderOAuth2Config
 | 
				
			||||||
from authentik.providers.oauth2.constants import ACR_AUTHENTIK_DEFAULT
 | 
					 | 
				
			||||||
from authentik.providers.oauth2.generators import (
 | 
					from authentik.providers.oauth2.generators import (
 | 
				
			||||||
    generate_client_id,
 | 
					    generate_client_id,
 | 
				
			||||||
    generate_client_secret,
 | 
					    generate_client_secret,
 | 
				
			||||||
@ -71,19 +67,14 @@ class SubModes(models.TextChoices):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class IssuerMode(models.TextChoices):
 | 
					 | 
				
			||||||
    """Configure how the `iss` field is created."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    GLOBAL = "global", _("Same identifier is used for all providers")
 | 
					 | 
				
			||||||
    PER_PROVIDER = "per_provider", _(
 | 
					 | 
				
			||||||
        "Each provider has a different issuer, based on the application slug."
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ResponseTypes(models.TextChoices):
 | 
					class ResponseTypes(models.TextChoices):
 | 
				
			||||||
    """Response Type required by the client."""
 | 
					    """Response Type required by the client."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    CODE = "code", _("code (Authorization Code Flow)")
 | 
					    CODE = "code", _("code (Authorization Code Flow)")
 | 
				
			||||||
 | 
					    CODE_ADFS = (
 | 
				
			||||||
 | 
					        "code#adfs",
 | 
				
			||||||
 | 
					        _("code (ADFS Compatibility Mode, sends id_token as access_token)"),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    ID_TOKEN = "id_token", _("id_token (Implicit Flow)")
 | 
					    ID_TOKEN = "id_token", _("id_token (Implicit Flow)")
 | 
				
			||||||
    ID_TOKEN_TOKEN = "id_token token", _("id_token token (Implicit Flow)")
 | 
					    ID_TOKEN_TOKEN = "id_token token", _("id_token token (Implicit Flow)")
 | 
				
			||||||
    CODE_TOKEN = "code token", _("code token (Hybrid Flow)")
 | 
					    CODE_TOKEN = "code token", _("code token (Hybrid Flow)")
 | 
				
			||||||
@ -149,6 +140,11 @@ class OAuth2Provider(Provider):
 | 
				
			|||||||
        verbose_name=_("Client Secret"),
 | 
					        verbose_name=_("Client Secret"),
 | 
				
			||||||
        default=generate_client_secret,
 | 
					        default=generate_client_secret,
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					    response_type = models.TextField(
 | 
				
			||||||
 | 
					        choices=ResponseTypes.choices,
 | 
				
			||||||
 | 
					        default=ResponseTypes.CODE,
 | 
				
			||||||
 | 
					        help_text=_(ResponseTypes.__doc__),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    jwt_alg = models.CharField(
 | 
					    jwt_alg = models.CharField(
 | 
				
			||||||
        max_length=10,
 | 
					        max_length=10,
 | 
				
			||||||
        choices=JWTAlgorithms.choices,
 | 
					        choices=JWTAlgorithms.choices,
 | 
				
			||||||
@ -194,13 +190,6 @@ class OAuth2Provider(Provider):
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    issuer_mode = models.TextField(
 | 
					 | 
				
			||||||
        choices=IssuerMode.choices,
 | 
					 | 
				
			||||||
        default=IssuerMode.PER_PROVIDER,
 | 
					 | 
				
			||||||
        help_text=_(
 | 
					 | 
				
			||||||
            ("Configure how the issuer field of the ID Token should be filled.")
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rsa_key = models.ForeignKey(
 | 
					    rsa_key = models.ForeignKey(
 | 
				
			||||||
        CertificateKeyPair,
 | 
					        CertificateKeyPair,
 | 
				
			||||||
@ -214,17 +203,19 @@ class OAuth2Provider(Provider):
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_refresh_token(
 | 
					    def create_refresh_token(
 | 
				
			||||||
        self, user: User, scope: List[str], request: HttpRequest
 | 
					        self, user: User, scope: List[str], id_token: Optional["IDToken"] = None
 | 
				
			||||||
    ) -> "RefreshToken":
 | 
					    ) -> "RefreshToken":
 | 
				
			||||||
        """Create and populate a RefreshToken object."""
 | 
					        """Create and populate a RefreshToken object."""
 | 
				
			||||||
        token = RefreshToken(
 | 
					        token = RefreshToken(
 | 
				
			||||||
            user=user,
 | 
					            user=user,
 | 
				
			||||||
            provider=self,
 | 
					            provider=self,
 | 
				
			||||||
 | 
					            access_token=uuid4().hex,
 | 
				
			||||||
            refresh_token=uuid4().hex,
 | 
					            refresh_token=uuid4().hex,
 | 
				
			||||||
            expires=timezone.now() + timedelta_from_string(self.token_validity),
 | 
					            expires=timezone.now() + timedelta_from_string(self.token_validity),
 | 
				
			||||||
            scope=scope,
 | 
					            scope=scope,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        token.access_token = token.create_access_token(user, request)
 | 
					        if id_token:
 | 
				
			||||||
 | 
					            token.id_token = id_token
 | 
				
			||||||
        return token
 | 
					        return token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_jwt_keys(self) -> List[Key]:
 | 
					    def get_jwt_keys(self) -> List[Key]:
 | 
				
			||||||
@ -236,11 +227,6 @@ class OAuth2Provider(Provider):
 | 
				
			|||||||
            # if the user selected RS256 but didn't select a
 | 
					            # if the user selected RS256 but didn't select a
 | 
				
			||||||
            # CertificateKeyPair, we fall back to HS256
 | 
					            # CertificateKeyPair, we fall back to HS256
 | 
				
			||||||
            if not self.rsa_key:
 | 
					            if not self.rsa_key:
 | 
				
			||||||
                Event.new(
 | 
					 | 
				
			||||||
                    EventAction.CONFIGURATION_ERROR,
 | 
					 | 
				
			||||||
                    provider=self,
 | 
					 | 
				
			||||||
                    message="Provider was configured for RS256, but no key was selected.",
 | 
					 | 
				
			||||||
                ).save()
 | 
					 | 
				
			||||||
                self.jwt_alg = JWTAlgorithms.HS256
 | 
					                self.jwt_alg = JWTAlgorithms.HS256
 | 
				
			||||||
                self.save()
 | 
					                self.save()
 | 
				
			||||||
            else:
 | 
					            else:
 | 
				
			||||||
@ -260,8 +246,6 @@ class OAuth2Provider(Provider):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_issuer(self, request: HttpRequest) -> Optional[str]:
 | 
					    def get_issuer(self, request: HttpRequest) -> Optional[str]:
 | 
				
			||||||
        """Get issuer, based on request"""
 | 
					        """Get issuer, based on request"""
 | 
				
			||||||
        if self.issuer_mode == IssuerMode.GLOBAL:
 | 
					 | 
				
			||||||
            return request.build_absolute_uri("/")
 | 
					 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            mountpoint = AuthentikProviderOAuth2Config.mountpoints[
 | 
					            mountpoint = AuthentikProviderOAuth2Config.mountpoints[
 | 
				
			||||||
                "authentik.providers.oauth2.urls"
 | 
					                "authentik.providers.oauth2.urls"
 | 
				
			||||||
@ -381,24 +365,12 @@ class AuthorizationCode(ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
        max_length=255, null=True, verbose_name=_("Code Challenge Method")
 | 
					        max_length=255, null=True, verbose_name=_("Code Challenge Method")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					 | 
				
			||||||
    def c_hash(self):
 | 
					 | 
				
			||||||
        """https://openid.net/specs/openid-connect-core-1_0.html#IDToken"""
 | 
					 | 
				
			||||||
        hashed_code = sha256(self.code.encode("ascii")).hexdigest().encode("ascii")
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
            base64.urlsafe_b64encode(
 | 
					 | 
				
			||||||
                binascii.unhexlify(hashed_code[: len(hashed_code) // 2])
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            .rstrip(b"=")
 | 
					 | 
				
			||||||
            .decode("ascii")
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    class Meta:
 | 
					    class Meta:
 | 
				
			||||||
        verbose_name = _("Authorization Code")
 | 
					        verbose_name = _("Authorization Code")
 | 
				
			||||||
        verbose_name_plural = _("Authorization Codes")
 | 
					        verbose_name_plural = _("Authorization Codes")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Authorization code for {self.provider} for user {self.user}"
 | 
					        return "{0} - {1}".format(self.provider, self.code)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
@ -418,15 +390,20 @@ class IDToken:
 | 
				
			|||||||
    exp: Optional[int] = None
 | 
					    exp: Optional[int] = None
 | 
				
			||||||
    iat: Optional[int] = None
 | 
					    iat: Optional[int] = None
 | 
				
			||||||
    auth_time: Optional[int] = None
 | 
					    auth_time: Optional[int] = None
 | 
				
			||||||
    acr: Optional[str] = ACR_AUTHENTIK_DEFAULT
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    c_hash: Optional[str] = None
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    nonce: Optional[str] = None
 | 
					    nonce: Optional[str] = None
 | 
				
			||||||
    at_hash: Optional[str] = None
 | 
					    at_hash: Optional[str] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    claims: Dict[str, Any] = field(default_factory=dict)
 | 
					    claims: Dict[str, Any] = field(default_factory=dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @staticmethod
 | 
				
			||||||
 | 
					    def from_dict(data: Dict[str, Any]) -> "IDToken":
 | 
				
			||||||
 | 
					        """Reconstruct ID Token from json dictionary"""
 | 
				
			||||||
 | 
					        token = IDToken()
 | 
				
			||||||
 | 
					        for key, value in data.items():
 | 
				
			||||||
 | 
					            setattr(token, key, value)
 | 
				
			||||||
 | 
					        return token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def to_dict(self) -> Dict[str, Any]:
 | 
					    def to_dict(self) -> Dict[str, Any]:
 | 
				
			||||||
        """Convert dataclass to dict, and update with keys from `claims`"""
 | 
					        """Convert dataclass to dict, and update with keys from `claims`"""
 | 
				
			||||||
        dic = asdict(self)
 | 
					        dic = asdict(self)
 | 
				
			||||||
@ -438,7 +415,9 @@ class IDToken:
 | 
				
			|||||||
class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
					class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
				
			||||||
    """OAuth2 Refresh Token"""
 | 
					    """OAuth2 Refresh Token"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    access_token = models.TextField(verbose_name=_("Access Token"))
 | 
					    access_token = models.CharField(
 | 
				
			||||||
 | 
					        max_length=255, unique=True, verbose_name=_("Access Token")
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    refresh_token = models.CharField(
 | 
					    refresh_token = models.CharField(
 | 
				
			||||||
        max_length=255, unique=True, verbose_name=_("Refresh Token")
 | 
					        max_length=255, unique=True, verbose_name=_("Refresh Token")
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
@ -453,7 +432,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
        """Load ID Token from json"""
 | 
					        """Load ID Token from json"""
 | 
				
			||||||
        if self._id_token:
 | 
					        if self._id_token:
 | 
				
			||||||
            raw_token = json.loads(self._id_token)
 | 
					            raw_token = json.loads(self._id_token)
 | 
				
			||||||
            return from_dict(IDToken, raw_token)
 | 
					            return IDToken.from_dict(raw_token)
 | 
				
			||||||
        return IDToken()
 | 
					        return IDToken()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @id_token.setter
 | 
					    @id_token.setter
 | 
				
			||||||
@ -461,7 +440,7 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
        self._id_token = json.dumps(asdict(value))
 | 
					        self._id_token = json.dumps(asdict(value))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __str__(self):
 | 
					    def __str__(self):
 | 
				
			||||||
        return f"Refresh Token for {self.provider} for user {self.access_token.user}"
 | 
					        return f"{self.provider} - {self.access_token}"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def at_hash(self):
 | 
					    def at_hash(self):
 | 
				
			||||||
@ -477,13 +456,6 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
            .decode("ascii")
 | 
					            .decode("ascii")
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_access_token(self, user: User, request: HttpRequest) -> str:
 | 
					 | 
				
			||||||
        """Create access token with a similar format as Okta, Keycloak, ADFS"""
 | 
					 | 
				
			||||||
        token = self.create_id_token(user, request).to_dict()
 | 
					 | 
				
			||||||
        token["cid"] = self.provider.client_id
 | 
					 | 
				
			||||||
        token["uid"] = uuid4().hex
 | 
					 | 
				
			||||||
        return self.provider.encode(token)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def create_id_token(self, user: User, request: HttpRequest) -> IDToken:
 | 
					    def create_id_token(self, user: User, request: HttpRequest) -> IDToken:
 | 
				
			||||||
        """Creates the id_token.
 | 
					        """Creates the id_token.
 | 
				
			||||||
        See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken"""
 | 
					        See: http://openid.net/specs/openid-connect-core-1_0.html#IDToken"""
 | 
				
			||||||
@ -510,11 +482,8 @@ class RefreshToken(ExpiringModel, BaseGrantModel):
 | 
				
			|||||||
        exp_time = int(
 | 
					        exp_time = int(
 | 
				
			||||||
            now + timedelta_from_string(self.provider.token_validity).seconds
 | 
					            now + timedelta_from_string(self.provider.token_validity).seconds
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # We use the timestamp of the user's last successful login (EventAction.LOGIN) for auth_time
 | 
					        user_auth_time = user.last_login or user.date_joined
 | 
				
			||||||
        auth_event = Event.objects.filter(
 | 
					        auth_time = int(dateformat.format(user_auth_time, "U"))
 | 
				
			||||||
            action=EventAction.LOGIN, user=get_user(user)
 | 
					 | 
				
			||||||
        ).latest("created")
 | 
					 | 
				
			||||||
        auth_time = int(dateformat.format(auth_event.created, "U"))
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        token = IDToken(
 | 
					        token = IDToken(
 | 
				
			||||||
            iss=self.provider.get_issuer(request),
 | 
					            iss=self.provider.get_issuer(request),
 | 
				
			||||||
 | 
				
			|||||||
@ -6,6 +6,7 @@ from typing import List, Optional, Tuple
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse, JsonResponse
 | 
					from django.http import HttpRequest, HttpResponse, JsonResponse
 | 
				
			||||||
from django.utils.cache import patch_vary_headers
 | 
					from django.utils.cache import patch_vary_headers
 | 
				
			||||||
 | 
					from jwkest.jwt import JWT
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.providers.oauth2.errors import BearerTokenError
 | 
					from authentik.providers.oauth2.errors import BearerTokenError
 | 
				
			||||||
@ -139,3 +140,17 @@ def protected_resource_view(scopes: List[str]):
 | 
				
			|||||||
        return view_wrapper
 | 
					        return view_wrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return wrapper
 | 
					    return wrapper
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def client_id_from_id_token(id_token):
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    Extracts the client id from a JSON Web Token (JWT).
 | 
				
			||||||
 | 
					    Returns a string or None.
 | 
				
			||||||
 | 
					    """
 | 
				
			||||||
 | 
					    payload = JWT().unpack(id_token).payload()
 | 
				
			||||||
 | 
					    aud = payload.get("aud", None)
 | 
				
			||||||
 | 
					    if aud is None:
 | 
				
			||||||
 | 
					        return None
 | 
				
			||||||
 | 
					    if isinstance(aud, list):
 | 
				
			||||||
 | 
					        return aud[0]
 | 
				
			||||||
 | 
					    return aud
 | 
				
			||||||
 | 
				
			|||||||
@ -1,19 +1,16 @@
 | 
				
			|||||||
"""authentik OAuth2 Authorization views"""
 | 
					"""authentik OAuth2 Authorization views"""
 | 
				
			||||||
from dataclasses import dataclass, field
 | 
					from dataclasses import dataclass, field
 | 
				
			||||||
from datetime import timedelta
 | 
					 | 
				
			||||||
from typing import List, Optional, Set
 | 
					from typing import List, Optional, Set
 | 
				
			||||||
from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
 | 
					from urllib.parse import parse_qs, urlencode, urlsplit, urlunsplit
 | 
				
			||||||
from uuid import uuid4
 | 
					from uuid import uuid4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from django.http import HttpRequest, HttpResponse
 | 
					from django.http import HttpRequest, HttpResponse
 | 
				
			||||||
from django.http.response import Http404
 | 
					 | 
				
			||||||
from django.shortcuts import get_object_or_404, redirect
 | 
					from django.shortcuts import get_object_or_404, redirect
 | 
				
			||||||
from django.utils import timezone
 | 
					from django.utils import timezone
 | 
				
			||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.audit.models import Event, EventAction
 | 
				
			||||||
from authentik.core.models import Application
 | 
					from authentik.core.models import Application
 | 
				
			||||||
from authentik.events.models import Event, EventAction
 | 
					 | 
				
			||||||
from authentik.events.utils import get_user
 | 
					 | 
				
			||||||
from authentik.flows.models import in_memory_stage
 | 
					from authentik.flows.models import in_memory_stage
 | 
				
			||||||
from authentik.flows.planner import (
 | 
					from authentik.flows.planner import (
 | 
				
			||||||
    PLAN_CONTEXT_APPLICATION,
 | 
					    PLAN_CONTEXT_APPLICATION,
 | 
				
			||||||
@ -26,10 +23,9 @@ from authentik.flows.views import SESSION_KEY_PLAN
 | 
				
			|||||||
from authentik.lib.utils.time import timedelta_from_string
 | 
					from authentik.lib.utils.time import timedelta_from_string
 | 
				
			||||||
from authentik.lib.utils.urls import redirect_with_qs
 | 
					from authentik.lib.utils.urls import redirect_with_qs
 | 
				
			||||||
from authentik.lib.views import bad_request_message
 | 
					from authentik.lib.views import bad_request_message
 | 
				
			||||||
from authentik.policies.views import PolicyAccessView, RequestValidationError
 | 
					from authentik.policies.views import PolicyAccessView
 | 
				
			||||||
from authentik.providers.oauth2.constants import (
 | 
					from authentik.providers.oauth2.constants import (
 | 
				
			||||||
    PROMPT_CONSNET,
 | 
					    PROMPT_CONSNET,
 | 
				
			||||||
    PROMPT_LOGIN,
 | 
					 | 
				
			||||||
    PROMPT_NONE,
 | 
					    PROMPT_NONE,
 | 
				
			||||||
    SCOPE_OPENID,
 | 
					    SCOPE_OPENID,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
@ -56,13 +52,11 @@ LOGGER = get_logger()
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
PLAN_CONTEXT_PARAMS = "params"
 | 
					PLAN_CONTEXT_PARAMS = "params"
 | 
				
			||||||
PLAN_CONTEXT_SCOPE_DESCRIPTIONS = "scope_descriptions"
 | 
					PLAN_CONTEXT_SCOPE_DESCRIPTIONS = "scope_descriptions"
 | 
				
			||||||
SESSION_NEEDS_LOGIN = "authentik_oauth2_needs_login"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSNET, PROMPT_LOGIN}
 | 
					ALLOWED_PROMPT_PARAMS = {PROMPT_NONE, PROMPT_CONSNET}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
# pylint: disable=too-many-instance-attributes
 | 
					 | 
				
			||||||
class OAuthAuthorizationParams:
 | 
					class OAuthAuthorizationParams:
 | 
				
			||||||
    """Parameteres required to authorize an OAuth Client"""
 | 
					    """Parameteres required to authorize an OAuth Client"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -71,16 +65,12 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
    response_type: str
 | 
					    response_type: str
 | 
				
			||||||
    scope: List[str]
 | 
					    scope: List[str]
 | 
				
			||||||
    state: str
 | 
					    state: str
 | 
				
			||||||
    nonce: Optional[str]
 | 
					    nonce: str
 | 
				
			||||||
    prompt: Set[str]
 | 
					    prompt: Set[str]
 | 
				
			||||||
    grant_type: str
 | 
					    grant_type: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    provider: OAuth2Provider = field(default_factory=OAuth2Provider)
 | 
					    provider: OAuth2Provider = field(default_factory=OAuth2Provider)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    request: Optional[str] = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    max_age: Optional[int] = None
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    code_challenge: Optional[str] = None
 | 
					    code_challenge: Optional[str] = None
 | 
				
			||||||
    code_challenge_method: Optional[str] = None
 | 
					    code_challenge_method: Optional[str] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -95,17 +85,16 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
        # Because in this endpoint we handle both GET
 | 
					        # Because in this endpoint we handle both GET
 | 
				
			||||||
        # and POST request.
 | 
					        # and POST request.
 | 
				
			||||||
        query_dict = request.POST if request.method == "POST" else request.GET
 | 
					        query_dict = request.POST if request.method == "POST" else request.GET
 | 
				
			||||||
        state = query_dict.get("state")
 | 
					 | 
				
			||||||
        redirect_uri = query_dict.get("redirect_uri", "")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        response_type = query_dict.get("response_type", "")
 | 
					        response_type = query_dict.get("response_type", "")
 | 
				
			||||||
        grant_type = None
 | 
					        grant_type = None
 | 
				
			||||||
        # Determine which flow to use.
 | 
					        # Determine which flow to use.
 | 
				
			||||||
        if response_type in [ResponseTypes.CODE]:
 | 
					        if response_type in [ResponseTypes.CODE, ResponseTypes.CODE_ADFS]:
 | 
				
			||||||
            grant_type = GrantTypes.AUTHORIZATION_CODE
 | 
					            grant_type = GrantTypes.AUTHORIZATION_CODE
 | 
				
			||||||
        elif response_type in [
 | 
					        elif response_type in [
 | 
				
			||||||
            ResponseTypes.ID_TOKEN,
 | 
					            ResponseTypes.ID_TOKEN,
 | 
				
			||||||
            ResponseTypes.ID_TOKEN_TOKEN,
 | 
					            ResponseTypes.ID_TOKEN_TOKEN,
 | 
				
			||||||
 | 
					            ResponseTypes.CODE_TOKEN,
 | 
				
			||||||
        ]:
 | 
					        ]:
 | 
				
			||||||
            grant_type = GrantTypes.IMPLICIT
 | 
					            grant_type = GrantTypes.IMPLICIT
 | 
				
			||||||
        elif response_type in [
 | 
					        elif response_type in [
 | 
				
			||||||
@ -118,22 +107,23 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
        # Grant type validation.
 | 
					        # Grant type validation.
 | 
				
			||||||
        if not grant_type:
 | 
					        if not grant_type:
 | 
				
			||||||
            LOGGER.warning("Invalid response type", type=response_type)
 | 
					            LOGGER.warning("Invalid response type", type=response_type)
 | 
				
			||||||
            raise AuthorizeError(redirect_uri, "unsupported_response_type", "", state)
 | 
					            raise AuthorizeError(
 | 
				
			||||||
 | 
					                query_dict.get("redirect_uri", ""),
 | 
				
			||||||
 | 
					                "unsupported_response_type",
 | 
				
			||||||
 | 
					                grant_type,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        max_age = query_dict.get("max_age")
 | 
					 | 
				
			||||||
        return OAuthAuthorizationParams(
 | 
					        return OAuthAuthorizationParams(
 | 
				
			||||||
            client_id=query_dict.get("client_id", ""),
 | 
					            client_id=query_dict.get("client_id", ""),
 | 
				
			||||||
            redirect_uri=redirect_uri,
 | 
					            redirect_uri=query_dict.get("redirect_uri", ""),
 | 
				
			||||||
            response_type=response_type,
 | 
					            response_type=response_type,
 | 
				
			||||||
            grant_type=grant_type,
 | 
					            grant_type=grant_type,
 | 
				
			||||||
            scope=query_dict.get("scope", "").split(),
 | 
					            scope=query_dict.get("scope", "").split(),
 | 
				
			||||||
            state=state,
 | 
					            state=query_dict.get("state", ""),
 | 
				
			||||||
            nonce=query_dict.get("nonce"),
 | 
					            nonce=query_dict.get("nonce", ""),
 | 
				
			||||||
            prompt=ALLOWED_PROMPT_PARAMS.intersection(
 | 
					            prompt=ALLOWED_PROMPT_PARAMS.intersection(
 | 
				
			||||||
                set(query_dict.get("prompt", "").split())
 | 
					                set(query_dict.get("prompt", "").split())
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            request=query_dict.get("request", None),
 | 
					 | 
				
			||||||
            max_age=int(max_age) if max_age else None,
 | 
					 | 
				
			||||||
            code_challenge=query_dict.get("code_challenge"),
 | 
					            code_challenge=query_dict.get("code_challenge"),
 | 
				
			||||||
            code_challenge_method=query_dict.get("code_challenge_method"),
 | 
					            code_challenge_method=query_dict.get("code_challenge_method"),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
@ -146,26 +136,15 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
        except OAuth2Provider.DoesNotExist:
 | 
					        except OAuth2Provider.DoesNotExist:
 | 
				
			||||||
            LOGGER.warning("Invalid client identifier", client_id=self.client_id)
 | 
					            LOGGER.warning("Invalid client identifier", client_id=self.client_id)
 | 
				
			||||||
            raise ClientIdError()
 | 
					            raise ClientIdError()
 | 
				
			||||||
        self.check_redirect_uri()
 | 
					        is_open_id = SCOPE_OPENID in self.scope
 | 
				
			||||||
        self.check_scope()
 | 
					 | 
				
			||||||
        self.check_nonce()
 | 
					 | 
				
			||||||
        self.check_code_challenge()
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_redirect_uri(self):
 | 
					        # Redirect URI validation.
 | 
				
			||||||
        """Redirect URI validation."""
 | 
					 | 
				
			||||||
        if not self.redirect_uri:
 | 
					        if not self.redirect_uri:
 | 
				
			||||||
            LOGGER.warning("Missing redirect uri.")
 | 
					            LOGGER.warning("Missing redirect uri.")
 | 
				
			||||||
            raise RedirectUriError()
 | 
					            raise RedirectUriError()
 | 
				
			||||||
        if self.redirect_uri.lower() not in [
 | 
					        if self.redirect_uri.lower() not in [
 | 
				
			||||||
            x.lower() for x in self.provider.redirect_uris.split()
 | 
					            x.lower() for x in self.provider.redirect_uris.split()
 | 
				
			||||||
        ]:
 | 
					        ]:
 | 
				
			||||||
            Event.new(
 | 
					 | 
				
			||||||
                EventAction.CONFIGURATION_ERROR,
 | 
					 | 
				
			||||||
                provider=self.provider,
 | 
					 | 
				
			||||||
                message="Invalid redirect URI was used.",
 | 
					 | 
				
			||||||
                client_used=self.redirect_uri,
 | 
					 | 
				
			||||||
                configured=self.provider.redirect_uris.split(),
 | 
					 | 
				
			||||||
            ).save()
 | 
					 | 
				
			||||||
            LOGGER.warning(
 | 
					            LOGGER.warning(
 | 
				
			||||||
                "Invalid redirect uri",
 | 
					                "Invalid redirect uri",
 | 
				
			||||||
                redirect_uri=self.redirect_uri,
 | 
					                redirect_uri=self.redirect_uri,
 | 
				
			||||||
@ -173,41 +152,34 @@ class OAuthAuthorizationParams:
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
            raise RedirectUriError()
 | 
					            raise RedirectUriError()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.request:
 | 
					        if not is_open_id and (
 | 
				
			||||||
            raise AuthorizeError(
 | 
					 | 
				
			||||||
                self.redirect_uri, "request_not_supported", self.grant_type, self.state
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def check_scope(self):
 | 
					 | 
				
			||||||
        """Ensure openid scope is set in Hybrid flows, or when requesting an id_token"""
 | 
					 | 
				
			||||||
        if SCOPE_OPENID not in self.scope and (
 | 
					 | 
				
			||||||
            self.grant_type == GrantTypes.HYBRID
 | 
					            self.grant_type == GrantTypes.HYBRID
 | 
				
			||||||
            or self.response_type
 | 
					            or self.response_type
 | 
				
			||||||
            in [ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN]
 | 
					            in [ResponseTypes.ID_TOKEN, ResponseTypes.ID_TOKEN_TOKEN]
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            LOGGER.warning("Missing 'openid' scope.")
 | 
					            LOGGER.warning("Missing 'openid' scope.")
 | 
				
			||||||
            raise AuthorizeError(
 | 
					            raise AuthorizeError(self.redirect_uri, "invalid_scope", self.grant_type)
 | 
				
			||||||
                self.redirect_uri, "invalid_scope", self.grant_type, self.state
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_nonce(self):
 | 
					        # Nonce parameter validation.
 | 
				
			||||||
        """Nonce parameter validation."""
 | 
					        if is_open_id and self.grant_type == GrantTypes.IMPLICIT and not self.nonce:
 | 
				
			||||||
        if not self.nonce:
 | 
					            raise AuthorizeError(self.redirect_uri, "invalid_request", self.grant_type)
 | 
				
			||||||
            self.nonce = self.state
 | 
					
 | 
				
			||||||
            LOGGER.warning("Using state as nonce for OpenID Request")
 | 
					        # Response type parameter validation.
 | 
				
			||||||
        if not self.nonce:
 | 
					        if is_open_id:
 | 
				
			||||||
            if SCOPE_OPENID in self.scope:
 | 
					            actual_response_type = self.provider.response_type
 | 
				
			||||||
                LOGGER.warning("Missing nonce for OpenID Request")
 | 
					            if "#" in self.provider.response_type:
 | 
				
			||||||
 | 
					                hash_index = actual_response_type.index("#")
 | 
				
			||||||
 | 
					                actual_response_type = actual_response_type[:hash_index]
 | 
				
			||||||
 | 
					            if self.response_type != actual_response_type:
 | 
				
			||||||
                raise AuthorizeError(
 | 
					                raise AuthorizeError(
 | 
				
			||||||
                    self.redirect_uri, "invalid_request", self.grant_type, self.state
 | 
					                    self.redirect_uri, "invalid_request", self.grant_type
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def check_code_challenge(self):
 | 
					        # PKCE validation of the transformation method.
 | 
				
			||||||
        """PKCE validation of the transformation method."""
 | 
					 | 
				
			||||||
        if self.code_challenge:
 | 
					        if self.code_challenge:
 | 
				
			||||||
            if not (self.code_challenge_method in ["plain", "S256"]):
 | 
					            if not (self.code_challenge_method in ["plain", "S256"]):
 | 
				
			||||||
                raise AuthorizeError(
 | 
					                raise AuthorizeError(
 | 
				
			||||||
                    self.redirect_uri, "invalid_request", self.grant_type, self.state
 | 
					                    self.redirect_uri, "invalid_request", self.grant_type
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_code(self, request: HttpRequest) -> AuthorizationCode:
 | 
					    def create_code(self, request: HttpRequest) -> AuthorizationCode:
 | 
				
			||||||
@ -253,7 +225,6 @@ class OAuthFulfillmentStage(StageView):
 | 
				
			|||||||
                    self.params.redirect_uri,
 | 
					                    self.params.redirect_uri,
 | 
				
			||||||
                    "consent_required",
 | 
					                    "consent_required",
 | 
				
			||||||
                    self.params.grant_type,
 | 
					                    self.params.grant_type,
 | 
				
			||||||
                    self.params.state,
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            Event.new(
 | 
					            Event.new(
 | 
				
			||||||
                EventAction.AUTHORIZE_APPLICATION,
 | 
					                EventAction.AUTHORIZE_APPLICATION,
 | 
				
			||||||
@ -267,12 +238,14 @@ class OAuthFulfillmentStage(StageView):
 | 
				
			|||||||
            return bad_request_message(request, error.description, title=error.error)
 | 
					            return bad_request_message(request, error.description, title=error.error)
 | 
				
			||||||
        except AuthorizeError as error:
 | 
					        except AuthorizeError as error:
 | 
				
			||||||
            self.executor.stage_invalid()
 | 
					            self.executor.stage_invalid()
 | 
				
			||||||
            return redirect(error.create_uri())
 | 
					            uri = error.create_uri(self.params.redirect_uri, self.params.state)
 | 
				
			||||||
 | 
					            return redirect(uri)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_response_uri(self) -> str:
 | 
					    def create_response_uri(self) -> str:
 | 
				
			||||||
        """Create a final Response URI the user is redirected to."""
 | 
					        """Create a final Response URI the user is redirected to."""
 | 
				
			||||||
        uri = urlsplit(self.params.redirect_uri)
 | 
					        uri = urlsplit(self.params.redirect_uri)
 | 
				
			||||||
        query_params = parse_qs(uri.query)
 | 
					        query_params = parse_qs(uri.query)
 | 
				
			||||||
 | 
					        query_fragment = {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        try:
 | 
					        try:
 | 
				
			||||||
            code = None
 | 
					            code = None
 | 
				
			||||||
@ -289,117 +262,75 @@ class OAuthFulfillmentStage(StageView):
 | 
				
			|||||||
                query_params["state"] = [
 | 
					                query_params["state"] = [
 | 
				
			||||||
                    str(self.params.state) if self.params.state else ""
 | 
					                    str(self.params.state) if self.params.state else ""
 | 
				
			||||||
                ]
 | 
					                ]
 | 
				
			||||||
 | 
					            elif self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
 | 
				
			||||||
                uri = uri._replace(query=urlencode(query_params, doseq=True))
 | 
					                token = self.provider.create_refresh_token(
 | 
				
			||||||
                return urlunsplit(uri)
 | 
					                    user=self.request.user,
 | 
				
			||||||
            if self.params.grant_type in [GrantTypes.IMPLICIT, GrantTypes.HYBRID]:
 | 
					                    scope=self.params.scope,
 | 
				
			||||||
                query_fragment = self.create_implicit_response(code)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                uri = uri._replace(
 | 
					 | 
				
			||||||
                    fragment=uri.fragment + urlencode(query_fragment, doseq=True),
 | 
					 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                return urlunsplit(uri)
 | 
					
 | 
				
			||||||
            raise OAuth2Error()
 | 
					                # Check if response_type must include access_token in the response.
 | 
				
			||||||
 | 
					                if self.params.response_type in [
 | 
				
			||||||
 | 
					                    ResponseTypes.ID_TOKEN_TOKEN,
 | 
				
			||||||
 | 
					                    ResponseTypes.CODE_ID_TOKEN_TOKEN,
 | 
				
			||||||
 | 
					                    ResponseTypes.ID_TOKEN,
 | 
				
			||||||
 | 
					                    ResponseTypes.CODE_TOKEN,
 | 
				
			||||||
 | 
					                ]:
 | 
				
			||||||
 | 
					                    query_fragment["access_token"] = token.access_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # We don't need id_token if it's an OAuth2 request.
 | 
				
			||||||
 | 
					                if SCOPE_OPENID in self.params.scope:
 | 
				
			||||||
 | 
					                    id_token = token.create_id_token(
 | 
				
			||||||
 | 
					                        user=self.request.user,
 | 
				
			||||||
 | 
					                        request=self.request,
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    id_token.nonce = self.params.nonce
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # Include at_hash when access_token is being returned.
 | 
				
			||||||
 | 
					                    if "access_token" in query_fragment:
 | 
				
			||||||
 | 
					                        id_token.at_hash = token.at_hash
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    # Check if response_type must include id_token in the response.
 | 
				
			||||||
 | 
					                    if self.params.response_type in [
 | 
				
			||||||
 | 
					                        ResponseTypes.ID_TOKEN,
 | 
				
			||||||
 | 
					                        ResponseTypes.ID_TOKEN_TOKEN,
 | 
				
			||||||
 | 
					                        ResponseTypes.CODE_ID_TOKEN,
 | 
				
			||||||
 | 
					                        ResponseTypes.CODE_ID_TOKEN_TOKEN,
 | 
				
			||||||
 | 
					                    ]:
 | 
				
			||||||
 | 
					                        query_fragment["id_token"] = self.provider.encode(
 | 
				
			||||||
 | 
					                            id_token.to_dict()
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                    token.id_token = id_token
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Store the token.
 | 
				
			||||||
 | 
					                token.save()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # Code parameter must be present if it's Hybrid Flow.
 | 
				
			||||||
 | 
					                if self.params.grant_type == GrantTypes.HYBRID:
 | 
				
			||||||
 | 
					                    query_fragment["code"] = code.code
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                query_fragment["token_type"] = "bearer"
 | 
				
			||||||
 | 
					                query_fragment["expires_in"] = timedelta_from_string(
 | 
				
			||||||
 | 
					                    self.provider.token_validity
 | 
				
			||||||
 | 
					                ).seconds
 | 
				
			||||||
 | 
					                query_fragment["state"] = self.params.state if self.params.state else ""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        except OAuth2Error as error:
 | 
					        except OAuth2Error as error:
 | 
				
			||||||
            LOGGER.exception("Error when trying to create response uri", error=error)
 | 
					            LOGGER.exception("Error when trying to create response uri", error=error)
 | 
				
			||||||
            raise AuthorizeError(
 | 
					            raise AuthorizeError(
 | 
				
			||||||
                self.params.redirect_uri,
 | 
					                self.params.redirect_uri, "server_error", self.params.grant_type
 | 
				
			||||||
                "server_error",
 | 
					 | 
				
			||||||
                self.params.grant_type,
 | 
					 | 
				
			||||||
                self.params.state,
 | 
					 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_implicit_response(self, code: Optional[AuthorizationCode]) -> dict:
 | 
					        uri = uri._replace(
 | 
				
			||||||
        """Create implicit response's URL Fragment dictionary"""
 | 
					            query=urlencode(query_params, doseq=True),
 | 
				
			||||||
        query_fragment = {}
 | 
					            fragment=uri.fragment + urlencode(query_fragment, doseq=True),
 | 
				
			||||||
 | 
					 | 
				
			||||||
        token = self.provider.create_refresh_token(
 | 
					 | 
				
			||||||
            user=self.request.user,
 | 
					 | 
				
			||||||
            scope=self.params.scope,
 | 
					 | 
				
			||||||
            request=self.request,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Check if response_type must include access_token in the response.
 | 
					        return urlunsplit(uri)
 | 
				
			||||||
        if self.params.response_type in [
 | 
					 | 
				
			||||||
            ResponseTypes.ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
            ResponseTypes.CODE_ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
            ResponseTypes.ID_TOKEN,
 | 
					 | 
				
			||||||
            ResponseTypes.CODE_TOKEN,
 | 
					 | 
				
			||||||
        ]:
 | 
					 | 
				
			||||||
            query_fragment["access_token"] = token.access_token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # We don't need id_token if it's an OAuth2 request.
 | 
					 | 
				
			||||||
        if SCOPE_OPENID in self.params.scope:
 | 
					 | 
				
			||||||
            id_token = token.create_id_token(
 | 
					 | 
				
			||||||
                user=self.request.user,
 | 
					 | 
				
			||||||
                request=self.request,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            id_token.nonce = self.params.nonce
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Include at_hash when access_token is being returned.
 | 
					 | 
				
			||||||
            if "access_token" in query_fragment:
 | 
					 | 
				
			||||||
                id_token.at_hash = token.at_hash
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if self.params.response_type in [
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_ID_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
            ]:
 | 
					 | 
				
			||||||
                id_token.c_hash = code.c_hash
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Check if response_type must include id_token in the response.
 | 
					 | 
				
			||||||
            if self.params.response_type in [
 | 
					 | 
				
			||||||
                ResponseTypes.ID_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_ID_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
            ]:
 | 
					 | 
				
			||||||
                query_fragment["id_token"] = self.provider.encode(id_token.to_dict())
 | 
					 | 
				
			||||||
            token.id_token = id_token
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Store the token.
 | 
					 | 
				
			||||||
        token.save()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # Code parameter must be present if it's Hybrid Flow.
 | 
					 | 
				
			||||||
        if self.params.grant_type == GrantTypes.HYBRID:
 | 
					 | 
				
			||||||
            query_fragment["code"] = code.code
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        query_fragment["token_type"] = "bearer"
 | 
					 | 
				
			||||||
        query_fragment["expires_in"] = timedelta_from_string(
 | 
					 | 
				
			||||||
            self.provider.token_validity
 | 
					 | 
				
			||||||
        ).seconds
 | 
					 | 
				
			||||||
        query_fragment["state"] = self.params.state if self.params.state else ""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return query_fragment
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class AuthorizationFlowInitView(PolicyAccessView):
 | 
					class AuthorizationFlowInitView(PolicyAccessView):
 | 
				
			||||||
    """OAuth2 Flow initializer, checks access to application and starts flow"""
 | 
					    """OAuth2 Flow initializer, checks access to application and starts flow"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    params: OAuthAuthorizationParams
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def pre_permission_check(self):
 | 
					 | 
				
			||||||
        """Check prompt parameter before checking permission/authentication,
 | 
					 | 
				
			||||||
        see https://openid.net/specs/openid-connect-core-1_0.html#rfc.section.3.1.2.6"""
 | 
					 | 
				
			||||||
        try:
 | 
					 | 
				
			||||||
            self.params = OAuthAuthorizationParams.from_request(self.request)
 | 
					 | 
				
			||||||
        except AuthorizeError as error:
 | 
					 | 
				
			||||||
            raise RequestValidationError(redirect(error.create_uri()))
 | 
					 | 
				
			||||||
        except OAuth2Error as error:
 | 
					 | 
				
			||||||
            raise RequestValidationError(
 | 
					 | 
				
			||||||
                bad_request_message(self.request, error.description, title=error.error)
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        except OAuth2Provider.DoesNotExist:
 | 
					 | 
				
			||||||
            raise Http404
 | 
					 | 
				
			||||||
        if PROMPT_NONE in self.params.prompt and not self.request.user.is_authenticated:
 | 
					 | 
				
			||||||
            # When "prompt" is set to "none" but the user is not logged in, show an error message
 | 
					 | 
				
			||||||
            error = AuthorizeError(
 | 
					 | 
				
			||||||
                self.params.redirect_uri,
 | 
					 | 
				
			||||||
                "login_required",
 | 
					 | 
				
			||||||
                self.params.grant_type,
 | 
					 | 
				
			||||||
                self.params.state,
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            raise RequestValidationError(redirect(error.create_uri()))
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def resolve_provider_application(self):
 | 
					    def resolve_provider_application(self):
 | 
				
			||||||
        client_id = self.request.GET.get("client_id")
 | 
					        client_id = self.request.GET.get("client_id")
 | 
				
			||||||
        self.provider = get_object_or_404(OAuth2Provider, client_id=client_id)
 | 
					        self.provider = get_object_or_404(OAuth2Provider, client_id=client_id)
 | 
				
			||||||
@ -407,27 +338,13 @@ class AuthorizationFlowInitView(PolicyAccessView):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=unused-argument
 | 
					    # pylint: disable=unused-argument
 | 
				
			||||||
    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
					    def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
 | 
				
			||||||
        """Start FlowPLanner, return to flow executor shell"""
 | 
					        """Check access to application, start FlowPLanner, return to flow executor shell"""
 | 
				
			||||||
        # After we've checked permissions, and the user has access, check if we need
 | 
					        # Extract params so we can save them in the plan context
 | 
				
			||||||
        # to re-authenticate the user
 | 
					        try:
 | 
				
			||||||
        if self.params.max_age:
 | 
					            params = OAuthAuthorizationParams.from_request(request)
 | 
				
			||||||
            current_age: timedelta = (
 | 
					        except (ClientIdError, RedirectUriError) as error:
 | 
				
			||||||
                timezone.now()
 | 
					            # pylint: disable=no-member
 | 
				
			||||||
                - Event.objects.filter(
 | 
					            return bad_request_message(request, error.description, title=error.error)
 | 
				
			||||||
                    action=EventAction.LOGIN, user=get_user(self.request.user)
 | 
					 | 
				
			||||||
                )
 | 
					 | 
				
			||||||
                .latest("created")
 | 
					 | 
				
			||||||
                .created
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
            if current_age.total_seconds() > self.params.max_age:
 | 
					 | 
				
			||||||
                return self.handle_no_permission()
 | 
					 | 
				
			||||||
        # If prompt=login, we need to re-authenticate the user regardless
 | 
					 | 
				
			||||||
        if (
 | 
					 | 
				
			||||||
            PROMPT_LOGIN in self.params.prompt
 | 
					 | 
				
			||||||
            and SESSION_NEEDS_LOGIN not in self.request.session
 | 
					 | 
				
			||||||
        ):
 | 
					 | 
				
			||||||
            self.request.session[SESSION_NEEDS_LOGIN] = True
 | 
					 | 
				
			||||||
            return self.handle_no_permission()
 | 
					 | 
				
			||||||
        # Regardless, we start the planner and return to it
 | 
					        # Regardless, we start the planner and return to it
 | 
				
			||||||
        planner = FlowPlanner(self.provider.authorization_flow)
 | 
					        planner = FlowPlanner(self.provider.authorization_flow)
 | 
				
			||||||
        # planner.use_cache = False
 | 
					        # planner.use_cache = False
 | 
				
			||||||
@ -438,9 +355,9 @@ class AuthorizationFlowInitView(PolicyAccessView):
 | 
				
			|||||||
                PLAN_CONTEXT_SSO: True,
 | 
					                PLAN_CONTEXT_SSO: True,
 | 
				
			||||||
                PLAN_CONTEXT_APPLICATION: self.application,
 | 
					                PLAN_CONTEXT_APPLICATION: self.application,
 | 
				
			||||||
                # OAuth2 related params
 | 
					                # OAuth2 related params
 | 
				
			||||||
                PLAN_CONTEXT_PARAMS: self.params,
 | 
					                PLAN_CONTEXT_PARAMS: params,
 | 
				
			||||||
                PLAN_CONTEXT_SCOPE_DESCRIPTIONS: UserInfoView().get_scope_descriptions(
 | 
					                PLAN_CONTEXT_SCOPE_DESCRIPTIONS: UserInfoView().get_scope_descriptions(
 | 
				
			||||||
                    self.params.scope
 | 
					                    params.scope
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                # Consent related params
 | 
					                # Consent related params
 | 
				
			||||||
                PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/oauth2/consent.html",
 | 
					                PLAN_CONTEXT_CONSENT_TEMPLATE: "providers/oauth2/consent.html",
 | 
				
			||||||
@ -448,7 +365,7 @@ class AuthorizationFlowInitView(PolicyAccessView):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
        # OpenID clients can specify a `prompt` parameter, and if its set to consent we
 | 
					        # OpenID clients can specify a `prompt` parameter, and if its set to consent we
 | 
				
			||||||
        # need to inject a consent stage
 | 
					        # need to inject a consent stage
 | 
				
			||||||
        if PROMPT_CONSNET in self.params.prompt:
 | 
					        if PROMPT_CONSNET in params.prompt:
 | 
				
			||||||
            if not any([isinstance(x, ConsentStageView) for x in plan.stages]):
 | 
					            if not any([isinstance(x, ConsentStageView) for x in plan.stages]):
 | 
				
			||||||
                # Plan does not have any consent stage, so we add an in-memory one
 | 
					                # Plan does not have any consent stage, so we add an in-memory one
 | 
				
			||||||
                stage = ConsentStage(
 | 
					                stage = ConsentStage(
 | 
				
			||||||
 | 
				
			|||||||
@ -7,18 +7,7 @@ from django.views import View
 | 
				
			|||||||
from structlog import get_logger
 | 
					from structlog import get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.models import Application
 | 
					from authentik.core.models import Application
 | 
				
			||||||
from authentik.providers.oauth2.constants import (
 | 
					from authentik.providers.oauth2.models import OAuth2Provider
 | 
				
			||||||
    ACR_AUTHENTIK_DEFAULT,
 | 
					 | 
				
			||||||
    GRANT_TYPE_AUTHORIZATION_CODE,
 | 
					 | 
				
			||||||
    GRANT_TYPE_REFRESH_TOKEN,
 | 
					 | 
				
			||||||
    SCOPE_OPENID,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from authentik.providers.oauth2.models import (
 | 
					 | 
				
			||||||
    GrantTypes,
 | 
					 | 
				
			||||||
    OAuth2Provider,
 | 
					 | 
				
			||||||
    ResponseTypes,
 | 
					 | 
				
			||||||
    ScopeMapping,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
LOGGER = get_logger()
 | 
					LOGGER = get_logger()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -31,13 +20,6 @@ class ProviderInfoView(View):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def get_info(self, provider: OAuth2Provider) -> Dict[str, Any]:
 | 
					    def get_info(self, provider: OAuth2Provider) -> Dict[str, Any]:
 | 
				
			||||||
        """Get dictionary for OpenID Connect information"""
 | 
					        """Get dictionary for OpenID Connect information"""
 | 
				
			||||||
        scopes = list(
 | 
					 | 
				
			||||||
            ScopeMapping.objects.filter(provider=provider).values_list(
 | 
					 | 
				
			||||||
                "scope_name", flat=True
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        if SCOPE_OPENID not in scopes:
 | 
					 | 
				
			||||||
            scopes.append(SCOPE_OPENID)
 | 
					 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
            "issuer": provider.get_issuer(self.request),
 | 
					            "issuer": provider.get_issuer(self.request),
 | 
				
			||||||
            "authorization_endpoint": self.request.build_absolute_uri(
 | 
					            "authorization_endpoint": self.request.build_absolute_uri(
 | 
				
			||||||
@ -58,25 +40,13 @@ class ProviderInfoView(View):
 | 
				
			|||||||
            "introspection_endpoint": self.request.build_absolute_uri(
 | 
					            "introspection_endpoint": self.request.build_absolute_uri(
 | 
				
			||||||
                reverse("authentik_providers_oauth2:token-introspection")
 | 
					                reverse("authentik_providers_oauth2:token-introspection")
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "response_types_supported": [
 | 
					            "response_types_supported": [provider.response_type],
 | 
				
			||||||
                ResponseTypes.CODE,
 | 
					 | 
				
			||||||
                ResponseTypes.ID_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_ID_TOKEN,
 | 
					 | 
				
			||||||
                ResponseTypes.CODE_ID_TOKEN_TOKEN,
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            "jwks_uri": self.request.build_absolute_uri(
 | 
					            "jwks_uri": self.request.build_absolute_uri(
 | 
				
			||||||
                reverse(
 | 
					                reverse(
 | 
				
			||||||
                    "authentik_providers_oauth2:jwks",
 | 
					                    "authentik_providers_oauth2:jwks",
 | 
				
			||||||
                    kwargs={"application_slug": provider.application.slug},
 | 
					                    kwargs={"application_slug": provider.application.slug},
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
            "grant_types_supported": [
 | 
					 | 
				
			||||||
                GRANT_TYPE_AUTHORIZATION_CODE,
 | 
					 | 
				
			||||||
                GRANT_TYPE_REFRESH_TOKEN,
 | 
					 | 
				
			||||||
                GrantTypes.IMPLICIT,
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
            "id_token_signing_alg_values_supported": [provider.jwt_alg],
 | 
					            "id_token_signing_alg_values_supported": [provider.jwt_alg],
 | 
				
			||||||
            # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
 | 
					            # See: http://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
 | 
				
			||||||
            "subject_types_supported": ["public"],
 | 
					            "subject_types_supported": ["public"],
 | 
				
			||||||
@ -84,13 +54,6 @@ class ProviderInfoView(View):
 | 
				
			|||||||
                "client_secret_post",
 | 
					                "client_secret_post",
 | 
				
			||||||
                "client_secret_basic",
 | 
					                "client_secret_basic",
 | 
				
			||||||
            ],
 | 
					            ],
 | 
				
			||||||
            "acr_values_supported": [ACR_AUTHENTIK_DEFAULT],
 | 
					 | 
				
			||||||
            "scopes_supported": scopes,
 | 
					 | 
				
			||||||
            # https://openid.net/specs/openid-connect-core-1_0.html#RequestObject
 | 
					 | 
				
			||||||
            "request_parameter_supported": False,
 | 
					 | 
				
			||||||
            # Because claims are dynamic and per-application, the only fixed Claim is "sub"
 | 
					 | 
				
			||||||
            "claims_supported": ["sub"],
 | 
					 | 
				
			||||||
            "claims_parameter_supported": False,
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # pylint: disable=unused-argument
 | 
					    # pylint: disable=unused-argument
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@ from authentik.providers.oauth2.models import (
 | 
				
			|||||||
    AuthorizationCode,
 | 
					    AuthorizationCode,
 | 
				
			||||||
    OAuth2Provider,
 | 
					    OAuth2Provider,
 | 
				
			||||||
    RefreshToken,
 | 
					    RefreshToken,
 | 
				
			||||||
 | 
					    ResponseTypes,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth
 | 
					from authentik.providers.oauth2.utils import TokenResponse, extract_client_auth
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -92,10 +93,7 @@ class TokenParams:
 | 
				
			|||||||
                self.refresh_token = RefreshToken.objects.get(
 | 
					                self.refresh_token = RefreshToken.objects.get(
 | 
				
			||||||
                    refresh_token=raw_token, provider=self.provider
 | 
					                    refresh_token=raw_token, provider=self.provider
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
                # https://tools.ietf.org/html/rfc6749#section-6
 | 
					
 | 
				
			||||||
                # Fallback to original token's scopes when none are given
 | 
					 | 
				
			||||||
                if self.scope == []:
 | 
					 | 
				
			||||||
                    self.scope = self.refresh_token.scope
 | 
					 | 
				
			||||||
            except RefreshToken.DoesNotExist:
 | 
					            except RefreshToken.DoesNotExist:
 | 
				
			||||||
                LOGGER.warning(
 | 
					                LOGGER.warning(
 | 
				
			||||||
                    "Refresh token does not exist",
 | 
					                    "Refresh token does not exist",
 | 
				
			||||||
@ -177,7 +175,6 @@ class TokenView(View):
 | 
				
			|||||||
        refresh_token = self.params.authorization_code.provider.create_refresh_token(
 | 
					        refresh_token = self.params.authorization_code.provider.create_refresh_token(
 | 
				
			||||||
            user=self.params.authorization_code.user,
 | 
					            user=self.params.authorization_code.user,
 | 
				
			||||||
            scope=self.params.authorization_code.scope,
 | 
					            scope=self.params.authorization_code.scope,
 | 
				
			||||||
            request=self.request,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.params.authorization_code.is_open_id:
 | 
					        if self.params.authorization_code.is_open_id:
 | 
				
			||||||
@ -205,6 +202,13 @@ class TokenView(View):
 | 
				
			|||||||
            "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
 | 
					            "id_token": refresh_token.provider.encode(refresh_token.id_token.to_dict()),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.params.provider.response_type == ResponseTypes.CODE_ADFS:
 | 
				
			||||||
 | 
					            # This seems to be expected by some OIDC Clients
 | 
				
			||||||
 | 
					            # namely VMware vCenter. This is not documented in any OpenID or OAuth2 Standard.
 | 
				
			||||||
 | 
					            # Maybe this should be a setting
 | 
				
			||||||
 | 
					            # in the future?
 | 
				
			||||||
 | 
					            response_dict["access_token"] = response_dict["id_token"]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return response_dict
 | 
					        return response_dict
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create_refresh_response_dic(self) -> Dict[str, Any]:
 | 
					    def create_refresh_response_dic(self) -> Dict[str, Any]:
 | 
				
			||||||
@ -221,7 +225,6 @@ class TokenView(View):
 | 
				
			|||||||
        refresh_token: RefreshToken = provider.create_refresh_token(
 | 
					        refresh_token: RefreshToken = provider.create_refresh_token(
 | 
				
			||||||
            user=self.params.refresh_token.user,
 | 
					            user=self.params.refresh_token.user,
 | 
				
			||||||
            scope=self.params.scope,
 | 
					            scope=self.params.scope,
 | 
				
			||||||
            request=self.request,
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # If the Token has an id_token it's an Authentication request.
 | 
					        # If the Token has an id_token it's an Authentication request.
 | 
				
			||||||
@ -245,7 +248,9 @@ class TokenView(View):
 | 
				
			|||||||
            "expires_in": timedelta_from_string(
 | 
					            "expires_in": timedelta_from_string(
 | 
				
			||||||
                refresh_token.provider.token_validity
 | 
					                refresh_token.provider.token_validity
 | 
				
			||||||
            ).seconds,
 | 
					            ).seconds,
 | 
				
			||||||
            "id_token": self.params.provider.encode(refresh_token.id_token.to_dict()),
 | 
					            "id_token": self.params.provider.encode(
 | 
				
			||||||
 | 
					                self.params.refresh_token.id_token.to_dict()
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return dic
 | 
					        return dic
 | 
				
			||||||
 | 
				
			|||||||
@ -6,7 +6,6 @@ from rest_framework.response import Response
 | 
				
			|||||||
from rest_framework.serializers import ModelSerializer, Serializer
 | 
					from rest_framework.serializers import ModelSerializer, Serializer
 | 
				
			||||||
from rest_framework.viewsets import ModelViewSet
 | 
					from rest_framework.viewsets import ModelViewSet
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.core.api.utils import MetaNameSerializer
 | 
					 | 
				
			||||||
from authentik.providers.oauth2.views.provider import ProviderInfoView
 | 
					from authentik.providers.oauth2.views.provider import ProviderInfoView
 | 
				
			||||||
from authentik.providers.proxy.models import ProxyProvider
 | 
					from authentik.providers.proxy.models import ProxyProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -34,7 +33,7 @@ class OpenIDConnectConfigurationSerializer(Serializer):
 | 
				
			|||||||
        raise NotImplementedError
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
 | 
					class ProxyProviderSerializer(ModelSerializer):
 | 
				
			||||||
    """ProxyProvider Serializer"""
 | 
					    """ProxyProvider Serializer"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, validated_data):
 | 
					    def create(self, validated_data):
 | 
				
			||||||
@ -61,8 +60,6 @@ class ProxyProviderSerializer(MetaNameSerializer, ModelSerializer):
 | 
				
			|||||||
            "basic_auth_enabled",
 | 
					            "basic_auth_enabled",
 | 
				
			||||||
            "basic_auth_password_attribute",
 | 
					            "basic_auth_password_attribute",
 | 
				
			||||||
            "basic_auth_user_attribute",
 | 
					            "basic_auth_user_attribute",
 | 
				
			||||||
            "verbose_name",
 | 
					 | 
				
			||||||
            "verbose_name_plural",
 | 
					 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -2,7 +2,6 @@
 | 
				
			|||||||
from typing import Dict
 | 
					from typing import Dict
 | 
				
			||||||
from urllib.parse import urlparse
 | 
					from urllib.parse import urlparse
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import DeploymentPort
 | 
					 | 
				
			||||||
from authentik.outposts.controllers.docker import DockerController
 | 
					from authentik.outposts.controllers.docker import DockerController
 | 
				
			||||||
from authentik.outposts.models import DockerServiceConnection, Outpost
 | 
					from authentik.outposts.models import DockerServiceConnection, Outpost
 | 
				
			||||||
from authentik.providers.proxy.models import ProxyProvider
 | 
					from authentik.providers.proxy.models import ProxyProvider
 | 
				
			||||||
@ -13,10 +12,10 @@ class ProxyDockerController(DockerController):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
					    def __init__(self, outpost: Outpost, connection: DockerServiceConnection):
 | 
				
			||||||
        super().__init__(outpost, connection)
 | 
					        super().__init__(outpost, connection)
 | 
				
			||||||
        self.deployment_ports = [
 | 
					        self.deployment_ports = {
 | 
				
			||||||
            DeploymentPort(4180, "http", "tcp"),
 | 
					            "http": 4180,
 | 
				
			||||||
            DeploymentPort(4443, "https", "tcp"),
 | 
					            "https": 4443,
 | 
				
			||||||
        ]
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _get_labels(self) -> Dict[str, str]:
 | 
					    def _get_labels(self) -> Dict[str, str]:
 | 
				
			||||||
        hosts = []
 | 
					        hosts = []
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,6 @@ from kubernetes.client.models.networking_v1beta1_ingress_rule import (
 | 
				
			|||||||
    NetworkingV1beta1IngressRule,
 | 
					    NetworkingV1beta1IngressRule,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from authentik.outposts.controllers.base import FIELD_MANAGER
 | 
					 | 
				
			||||||
from authentik.outposts.controllers.k8s.base import (
 | 
					from authentik.outposts.controllers.k8s.base import (
 | 
				
			||||||
    KubernetesObjectReconciler,
 | 
					    KubernetesObjectReconciler,
 | 
				
			||||||
    NeedsUpdate,
 | 
					    NeedsUpdate,
 | 
				
			||||||
@ -40,7 +39,6 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
				
			|||||||
    def reconcile(
 | 
					    def reconcile(
 | 
				
			||||||
        self, current: NetworkingV1beta1Ingress, reference: NetworkingV1beta1Ingress
 | 
					        self, current: NetworkingV1beta1Ingress, reference: NetworkingV1beta1Ingress
 | 
				
			||||||
    ):
 | 
					    ):
 | 
				
			||||||
        super().reconcile(current, reference)
 | 
					 | 
				
			||||||
        # Create a list of all expected host and tls hosts
 | 
					        # Create a list of all expected host and tls hosts
 | 
				
			||||||
        expected_hosts = []
 | 
					        expected_hosts = []
 | 
				
			||||||
        expected_hosts_tls = []
 | 
					        expected_hosts_tls = []
 | 
				
			||||||
@ -76,13 +74,11 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
				
			|||||||
            # goes to the same pod
 | 
					            # goes to the same pod
 | 
				
			||||||
            "nginx.ingress.kubernetes.io/affinity": "cookie",
 | 
					            "nginx.ingress.kubernetes.io/affinity": "cookie",
 | 
				
			||||||
            "traefik.ingress.kubernetes.io/affinity": "true",
 | 
					            "traefik.ingress.kubernetes.io/affinity": "true",
 | 
				
			||||||
            "nginx.ingress.kubernetes.io/proxy-buffers-number": "4",
 | 
					 | 
				
			||||||
            "nginx.ingress.kubernetes.io/proxy-buffer-size": "16k",
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        annotations.update(
 | 
					        annotations.update(
 | 
				
			||||||
            self.controller.outpost.config.kubernetes_ingress_annotations
 | 
					            self.controller.outpost.config.kubernetes_ingress_annotations
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return annotations
 | 
					        return dict()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get_reference_object(self) -> NetworkingV1beta1Ingress:
 | 
					    def get_reference_object(self) -> NetworkingV1beta1Ingress:
 | 
				
			||||||
        """Get deployment object for outpost"""
 | 
					        """Get deployment object for outpost"""
 | 
				
			||||||
@ -106,7 +102,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
				
			|||||||
                        NetworkingV1beta1HTTPIngressPath(
 | 
					                        NetworkingV1beta1HTTPIngressPath(
 | 
				
			||||||
                            backend=NetworkingV1beta1IngressBackend(
 | 
					                            backend=NetworkingV1beta1IngressBackend(
 | 
				
			||||||
                                service_name=self.name,
 | 
					                                service_name=self.name,
 | 
				
			||||||
                                service_port="http",
 | 
					                                service_port=self.controller.deployment_ports["http"],
 | 
				
			||||||
                            ),
 | 
					                            ),
 | 
				
			||||||
                            path="/",
 | 
					                            path="/",
 | 
				
			||||||
                        )
 | 
					                        )
 | 
				
			||||||
@ -126,9 +122,7 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def create(self, reference: NetworkingV1beta1Ingress):
 | 
					    def create(self, reference: NetworkingV1beta1Ingress):
 | 
				
			||||||
        return self.api.create_namespaced_ingress(
 | 
					        return self.api.create_namespaced_ingress(self.namespace, reference)
 | 
				
			||||||
            self.namespace, reference, field_manager=FIELD_MANAGER
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self, reference: NetworkingV1beta1Ingress):
 | 
					    def delete(self, reference: NetworkingV1beta1Ingress):
 | 
				
			||||||
        return self.api.delete_namespaced_ingress(
 | 
					        return self.api.delete_namespaced_ingress(
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,4 @@
 | 
				
			|||||||
"""Proxy Provider Kubernetes Contoller"""
 | 
					"""Proxy Provider Kubernetes Contoller"""
 | 
				
			||||||
from authentik.outposts.controllers.base import DeploymentPort
 | 
					 | 
				
			||||||
from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
					from authentik.outposts.controllers.kubernetes import KubernetesController
 | 
				
			||||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost
 | 
					from authentik.outposts.models import KubernetesServiceConnection, Outpost
 | 
				
			||||||
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
 | 
					from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
 | 
				
			||||||
@ -10,9 +9,9 @@ class ProxyKubernetesController(KubernetesController):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
					    def __init__(self, outpost: Outpost, connection: KubernetesServiceConnection):
 | 
				
			||||||
        super().__init__(outpost, connection)
 | 
					        super().__init__(outpost, connection)
 | 
				
			||||||
        self.deployment_ports = [
 | 
					        self.deployment_ports = {
 | 
				
			||||||
            DeploymentPort(4180, "http", "tcp"),
 | 
					            "http": 4180,
 | 
				
			||||||
            DeploymentPort(4443, "https", "tcp"),
 | 
					            "https": 4443,
 | 
				
			||||||
        ]
 | 
					        }
 | 
				
			||||||
        self.reconcilers["ingress"] = IngressReconciler
 | 
					        self.reconcilers["ingress"] = IngressReconciler
 | 
				
			||||||
        self.reconcile_order.append("ingress")
 | 
					        self.reconcile_order.append("ingress")
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,7 @@ from authentik.providers.proxy.models import ProxyProvider
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ProxyProviderForm(forms.ModelForm):
 | 
					class ProxyProviderForm(forms.ModelForm):
 | 
				
			||||||
    """Proxy Provider form"""
 | 
					    """Security Gateway Provider form"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    instance: ProxyProvider
 | 
					    instance: ProxyProvider
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,7 @@ from authentik.providers.oauth2.models import (
 | 
				
			|||||||
    ClientTypes,
 | 
					    ClientTypes,
 | 
				
			||||||
    JWTAlgorithms,
 | 
					    JWTAlgorithms,
 | 
				
			||||||
    OAuth2Provider,
 | 
					    OAuth2Provider,
 | 
				
			||||||
 | 
					    ResponseTypes,
 | 
				
			||||||
    ScopeMapping,
 | 
					    ScopeMapping,
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -126,6 +127,7 @@ class ProxyProvider(OutpostModel, OAuth2Provider):
 | 
				
			|||||||
    def set_oauth_defaults(self):
 | 
					    def set_oauth_defaults(self):
 | 
				
			||||||
        """Ensure all OAuth2-related settings are correct"""
 | 
					        """Ensure all OAuth2-related settings are correct"""
 | 
				
			||||||
        self.client_type = ClientTypes.CONFIDENTIAL
 | 
					        self.client_type = ClientTypes.CONFIDENTIAL
 | 
				
			||||||
 | 
					        self.response_type = ResponseTypes.CODE
 | 
				
			||||||
        self.jwt_alg = JWTAlgorithms.RS256
 | 
					        self.jwt_alg = JWTAlgorithms.RS256
 | 
				
			||||||
        self.rsa_key = CertificateKeyPair.objects.first()
 | 
					        self.rsa_key = CertificateKeyPair.objects.first()
 | 
				
			||||||
        scopes = ScopeMapping.objects.filter(
 | 
					        scopes = ScopeMapping.objects.filter(
 | 
				
			||||||
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user