Compare commits
57 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
5a7508d2e0 | |||
9c31ea1aa6 | |||
18211a2033 | |||
b4cfc56e5e | |||
8e797fa76b | |||
1b91543add | |||
1cd59be8dc | |||
66bb68a747 | |||
aa4f7fb2b6 | |||
4f1c11c5ef | |||
add7a80fdc | |||
aac91c2e9d | |||
85e86351cd | |||
d767504474 | |||
f84cd6208c | |||
1ec540ea9a | |||
29fe731bbf | |||
26e66969c9 | |||
fe629f8b51 | |||
72b7642c5a | |||
a97f842112 | |||
16e6e4c3b7 | |||
dc0d715885 | |||
7ecd57ecff | |||
0cb4d64b57 | |||
a4fd58a0db | |||
fb6e8ca1eb | |||
4c41948e75 | |||
a5c8caf909 | |||
970655ab21 | |||
c8c7202c61 | |||
a3981dd3cd | |||
affafc31cf | |||
602aed674b | |||
e6b515e3f7 | |||
36eaecfdec | |||
3973efae19 | |||
d8492e0df5 | |||
b64da0dd28 | |||
c3ae3e02f3 | |||
7c6a96394b | |||
0fe43f8319 | |||
7e32723748 | |||
577aa7ba79 | |||
b752540800 | |||
64c8ca9b5d | |||
5552e0ffa7 | |||
e7b7bfddd6 | |||
28f970c795 | |||
d1dbdfa9fe | |||
c4f4e3eac7 | |||
f21ebf5488 | |||
5615613ed1 | |||
669329e49c | |||
0587ab26e8 | |||
3c9cc9d421 | |||
1972464a20 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.7.1-rc2
|
||||
current_version = 2021.7.3
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)\-?(?P<release>.*)
|
||||
|
19
.github/pull_request_template.md
vendored
Normal file
19
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
<!--
|
||||
👋 Hello there! Welcome.
|
||||
|
||||
Please check the [Contributing guidelines](https://github.com/goauthentik/authentik/blob/master/CONTRIBUTING.md#how-can-i-contribute).
|
||||
-->
|
||||
|
||||
# Details
|
||||
* **Does this resolve an issue?**
|
||||
Resolves #
|
||||
|
||||
## Changes
|
||||
### New Features
|
||||
* Adds feature which does x, y, and z.
|
||||
|
||||
### Breaking Changes
|
||||
* Adds breaking change which causes \<issue\>.
|
||||
|
||||
## Additional
|
||||
Any further notes or comments you want to make.
|
22
.github/workflows/release.yml
vendored
22
.github/workflows/release.yml
vendored
@ -33,14 +33,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.7.1-rc2,
|
||||
beryju/authentik:2021.7.3,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.7.1-rc2,
|
||||
ghcr.io/goauthentik/server:2021.7.3,
|
||||
ghcr.io/goauthentik/server:latest
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.1-rc2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.3', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik:latest
|
||||
docker tag beryju/authentik:latest beryju/authentik:stable
|
||||
@ -75,14 +75,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-proxy:2021.7.1-rc2,
|
||||
beryju/authentik-proxy:2021.7.3,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.7.1-rc2,
|
||||
ghcr.io/goauthentik/proxy:2021.7.3,
|
||||
ghcr.io/goauthentik/proxy:latest
|
||||
file: proxy.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.1-rc2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.3', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-proxy:latest
|
||||
docker tag beryju/authentik-proxy:latest beryju/authentik-proxy:stable
|
||||
@ -117,14 +117,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik-ldap:2021.7.1-rc2,
|
||||
beryju/authentik-ldap:2021.7.3,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.7.1-rc2,
|
||||
ghcr.io/goauthentik/ldap:2021.7.3,
|
||||
ghcr.io/goauthentik/ldap:latest
|
||||
file: ldap.Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
- name: Building Docker Image (stable)
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.1-rc2', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.3', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -176,7 +176,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.7.1-rc2
|
||||
version: authentik@2021.7.3
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: /static/dist
|
||||
url_prefix: '~/static/dist'
|
||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -200,4 +200,4 @@ media/
|
||||
*mmdb
|
||||
|
||||
.idea/
|
||||
api/
|
||||
/api/
|
||||
|
128
CODE_OF_CONDUCT.md
Normal file
128
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,128 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in our
|
||||
community a harassment-free experience for everyone, regardless of age, body
|
||||
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
||||
identity and expression, level of experience, education, socio-economic status,
|
||||
nationality, personal appearance, race, religion, or sexual identity
|
||||
and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming,
|
||||
diverse, inclusive, and healthy community.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our
|
||||
community include:
|
||||
|
||||
* Demonstrating empathy and kindness toward other people
|
||||
* Being respectful of differing opinions, viewpoints, and experiences
|
||||
* Giving and gracefully accepting constructive feedback
|
||||
* Accepting responsibility and apologizing to those affected by our mistakes,
|
||||
and learning from the experience
|
||||
* Focusing on what is best not just for us as individuals, but for the
|
||||
overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
* The use of sexualized language or imagery, and sexual attention or
|
||||
advances of any kind
|
||||
* Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or email
|
||||
address, without their explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Enforcement Responsibilities
|
||||
|
||||
Community leaders are responsible for clarifying and enforcing our standards of
|
||||
acceptable behavior and will take appropriate and fair corrective action in
|
||||
response to any behavior that they deem inappropriate, threatening, offensive,
|
||||
or harmful.
|
||||
|
||||
Community leaders have the right and responsibility to remove, edit, or reject
|
||||
comments, commits, code, wiki edits, issues, and other contributions that are
|
||||
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
||||
decisions when appropriate.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all community spaces, and also applies when
|
||||
an individual is officially representing the community in public spaces.
|
||||
Examples of representing our community include using an official e-mail address,
|
||||
posting via an official social media account, or acting as an appointed
|
||||
representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported to the community leaders responsible for enforcement at
|
||||
hello@beryju.org.
|
||||
All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All community leaders are obligated to respect the privacy and security of the
|
||||
reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Community leaders will follow these Community Impact Guidelines in determining
|
||||
the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed
|
||||
unprofessional or unwelcome in the community.
|
||||
|
||||
**Consequence**: A private, written warning from community leaders, providing
|
||||
clarity around the nature of the violation and an explanation of why the
|
||||
behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
|
||||
**Community Impact**: A violation through a single incident or series
|
||||
of actions.
|
||||
|
||||
**Consequence**: A warning with consequences for continued behavior. No
|
||||
interaction with the people involved, including unsolicited interaction with
|
||||
those enforcing the Code of Conduct, for a specified period of time. This
|
||||
includes avoiding interactions in community spaces as well as external channels
|
||||
like social media. Violating these terms may lead to a temporary or
|
||||
permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
|
||||
**Community Impact**: A serious violation of community standards, including
|
||||
sustained inappropriate behavior.
|
||||
|
||||
**Consequence**: A temporary ban from any sort of interaction or public
|
||||
communication with the community for a specified period of time. No public or
|
||||
private interaction with the people involved, including unsolicited interaction
|
||||
with those enforcing the Code of Conduct, is allowed during this period.
|
||||
Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
|
||||
**Community Impact**: Demonstrating a pattern of violation of community
|
||||
standards, including sustained inappropriate behavior, harassment of an
|
||||
individual, or aggression toward or disparagement of classes of individuals.
|
||||
|
||||
**Consequence**: A permanent ban from any sort of public interaction within
|
||||
the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
||||
version 2.0, available at
|
||||
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
||||
enforcement ladder](https://github.com/mozilla/diversity).
|
||||
|
||||
[homepage]: https://www.contributor-covenant.org
|
||||
|
||||
For answers to common questions about this code of conduct, see the FAQ at
|
||||
https://www.contributor-covenant.org/faq. Translations are available at
|
||||
https://www.contributor-covenant.org/translations.
|
180
CONTRIBUTING.md
Normal file
180
CONTRIBUTING.md
Normal file
@ -0,0 +1,180 @@
|
||||
# Contributing to authentik
|
||||
|
||||
:+1::tada: Thanks for taking the time to contribute! :tada::+1:
|
||||
|
||||
The following is a set of guidelines for contributing to authentik and its components, which are hosted in the [goauthentik Organization](https://github.com/goauthentik) on GitHub. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
|
||||
|
||||
#### Table Of Contents
|
||||
|
||||
[Code of Conduct](#code-of-conduct)
|
||||
|
||||
[I don't want to read this whole thing, I just have a question!!!](#i-dont-want-to-read-this-whole-thing-i-just-have-a-question)
|
||||
|
||||
[What should I know before I get started?](#what-should-i-know-before-i-get-started)
|
||||
* [Atom and Packages](#atom-and-packages)
|
||||
* [Atom Design Decisions](#design-decisions)
|
||||
|
||||
[How Can I Contribute?](#how-can-i-contribute)
|
||||
* [Reporting Bugs](#reporting-bugs)
|
||||
* [Suggesting Enhancements](#suggesting-enhancements)
|
||||
* [Your First Code Contribution](#your-first-code-contribution)
|
||||
* [Pull Requests](#pull-requests)
|
||||
|
||||
[Styleguides](#styleguides)
|
||||
* [Git Commit Messages](#git-commit-messages)
|
||||
* [JavaScript Styleguide](#javascript-styleguide)
|
||||
* [CoffeeScript Styleguide](#coffeescript-styleguide)
|
||||
* [Specs Styleguide](#specs-styleguide)
|
||||
* [Documentation Styleguide](#documentation-styleguide)
|
||||
|
||||
[Additional Notes](#additional-notes)
|
||||
* [Issue and Pull Request Labels](#issue-and-pull-request-labels)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Basically, don't be a dickhead. This is an open-source non-profit project, that is made in the free time of Volunteers. If there's something you dislike or think can be done better, tell us! We'd love to hear any suggestions for improvement.
|
||||
|
||||
## I don't want to read this whole thing I just have a question!!!
|
||||
|
||||
Either [create a question on GitHub](https://github.com/goauthentik/authentik/issues/new?assignees=&labels=question&template=question.md&title=) or join [the Discord server](https://discord.gg/jg33eMhnj6)
|
||||
|
||||
## What should I know before I get started?
|
||||
|
||||
### The components
|
||||
|
||||
authentik consists of a few larger components:
|
||||
|
||||
- *authentik* the actual application server, is described below.
|
||||
- *outpost-proxy* is a Go application based on a forked version of oauth2_proxy, which does identity-aware reverse proxying.
|
||||
- *outpost-ldap* is a Go LDAP server that uses the *authentik* application server as its backend
|
||||
- *web* is the web frontend, both for administrating and using authentik. It is written in TypeScript using lit-html and the PatternFly CSS Library.
|
||||
- *website* is the Website/documentation, which uses docusaurus.
|
||||
|
||||
### authentik's structure
|
||||
|
||||
authentik is at it's very core a Django project. It consists of many individual django applications. These applications are intended to separate concerns, and they may share code between each other.
|
||||
|
||||
These are the current packages:
|
||||
<a id="authentik-packages"/>
|
||||
|
||||
```
|
||||
authentik
|
||||
├── admin - Administrative tasks and APIs, no models (Version updates, Metrics, system tasks)
|
||||
├── api - General API Configuration (Routes, Schema and general API utilities)
|
||||
├── core - Core authentik functionality, central routes, core Models
|
||||
├── crypto - Cryptography, currently used to generate and hold Certificates and Private Keys
|
||||
├── events - Event Log, middleware and signals to generate signals
|
||||
├── flows - Flows, the FlowPlanner and the FlowExecutor, used for all flows for authentication, authorization, etc
|
||||
├── lib - Generic library of functions, few dependencies on other packages.
|
||||
├── managed - Handle managed models and their state.
|
||||
├── outposts - Configure and deploy outposts on kubernetes and docker.
|
||||
├── policies - General PolicyEngine
|
||||
│ ├── dummy - A Dummy policy used for testing
|
||||
│ ├── event_matcher - Match events based on different criteria
|
||||
│ ├── expiry - Check when a user's password was last set
|
||||
│ ├── expression - Execute any arbitrary python code
|
||||
│ ├── hibp - Check a password against HaveIBeenPwned
|
||||
│ ├── password - Check a password against several rules
|
||||
│ └── reputation - Check the user's/client's reputation
|
||||
├── providers
|
||||
│ ├── ldap - Provide LDAP access to authentik users/groups using an outpost
|
||||
│ ├── oauth2 - OIDC-compliant OAuth2 provider
|
||||
│ ├── proxy - Provides an identity-aware proxy using an outpost
|
||||
│ └── saml - SAML2 Provider
|
||||
├── recovery - Generate keys to use in case you lock yourself out
|
||||
├── root - Root django application, contains global settings and routes
|
||||
├── sources
|
||||
│ ├── ldap - Sync LDAP users from OpenLDAP or Active Directory into authentik
|
||||
│ ├── oauth - OAuth1 and OAuth2 Source
|
||||
│ ├── plex - Plex source
|
||||
│ └── saml - SAML2 Source
|
||||
├── stages
|
||||
│ ├── authenticator_duo - Configure a DUO authenticator
|
||||
│ ├── authenticator_static - Configure TOTP backup keys
|
||||
│ ├── authenticator_totp - Configure a TOTP authenticator
|
||||
│ ├── authenticator_validate - Validate any authenticator
|
||||
│ ├── authenticator_webauthn - Configure a WebAuthn authenticator
|
||||
│ ├── captcha - Make the user pass a captcha
|
||||
│ ├── consent - Let the user decide if they want to consent to an action
|
||||
│ ├── deny - Static deny, can be used with policies
|
||||
│ ├── dummy - Dummy stage to test
|
||||
│ ├── email - Send the user an email and block execution until they click the link
|
||||
│ ├── identification - Identify a user with any combination of fields
|
||||
│ ├── invitation - Invitation system to limit flows to certain users
|
||||
│ ├── password - Password authentication
|
||||
│ ├── prompt - Arbitrary prompts
|
||||
│ ├── user_delete - Delete the currently pending user
|
||||
│ ├── user_login - Login the currently pending user
|
||||
│ ├── user_logout - Logout the currently pending user
|
||||
│ └── user_write - Write any currenetly pending data to the user.
|
||||
└── tenants - Soft tennancy, configure defaults and branding per domain
|
||||
```
|
||||
|
||||
This django project is running in gunicorn, which spawns multiple workers and threads. Gunicorn is run from a lightweight Go application which reverse-proxies it, handles static files and will eventually gain more functionality as more code is migrated to go.
|
||||
|
||||
There are also several background tasks which run in Celery, the root celery application is defined in `authentik.root.celery`.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
This section guides you through submitting a bug report for authentik. Following these guidelines helps maintainers and the community understand your report, reproduce the behavior, and find related reports.
|
||||
|
||||
Whenever authentik encounters an error, it will be logged as an Event with the type `system_exception`. This event type has a button to directly open a pre-filled GitHub issue form.
|
||||
|
||||
This form will have the full stack trace of the error that ocurred and shouldn't contain any sensitive data.
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
This section guides you through submitting an enhancement suggestion for authentik, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion and find related suggestions.
|
||||
|
||||
When you are creating an enhancement suggestion, please fill in [the template](https://github.com/goauthentik/authentik/issues/new?assignees=&labels=enhancement&template=feature_request.md&title=), including the steps that you imagine you would take if the feature you're requesting existed.
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
#### Local development
|
||||
|
||||
authentik can be run locally, all though depending on which part you want to work on, different pre-requisites are required.
|
||||
|
||||
This is documented in the [developer docs](https://goauthentik.io/developer-docs/)
|
||||
|
||||
### Pull Requests
|
||||
|
||||
The process described here has several goals:
|
||||
|
||||
- Maintain authentik's quality
|
||||
- Fix problems that are important to users
|
||||
- Engage the community in working toward the best possible authentik
|
||||
- Enable a sustainable system for authentik's maintainers to review contributions
|
||||
|
||||
Please follow these steps to have your contribution considered by the maintainers:
|
||||
|
||||
1. Follow the [styleguides](#styleguides)
|
||||
2. After you submit your pull request, verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing <details><summary>What if the status checks are failing?</summary>If a status check is failing, and you believe that the failure is unrelated to your change, please leave a comment on the pull request explaining why you believe the failure is unrelated. A maintainer will re-run the status check for you. If we conclude that the failure was a false positive, then we will open an issue to track that problem with our status check suite.</details>
|
||||
3. Ensure your Code has tests. While it is not always possible to test every single case, the majority of the code should be tested.
|
||||
|
||||
While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted.
|
||||
|
||||
## Styleguides
|
||||
|
||||
### Git Commit Messages
|
||||
|
||||
* Use the format of `<package>: <verb> <description>`
|
||||
- See [here](#authentik-packages) for `package`
|
||||
- Example: `providers/saml2: fix parsing of requests`
|
||||
* Reference issues and pull requests liberally after the first line
|
||||
|
||||
### Python Styleguide
|
||||
|
||||
All Python code is linted with [black](https://black.readthedocs.io/en/stable/), [PyLint](https://www.pylint.org/) and [isort](https://pycqa.github.io/isort/).
|
||||
|
||||
authentik runs on Python 3.9 at the time of writing this.
|
||||
|
||||
* Use native type-annotations wherever possible.
|
||||
* Add meaningful docstrings when possible.
|
||||
* Ensure any database migrations work properly from the last stable version (this is checked via CI)
|
||||
* If your code changes central functions, make sure nothing else is broken.
|
||||
|
||||
### Documentation Styleguide
|
||||
|
||||
* Use [MDX](https://mdxjs.com/) whenever appropriate.
|
68
Pipfile.lock
generated
68
Pipfile.lock
generated
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:0ee91710160166be01acdb7f57cf4f32876cc2e51a54d17e42a3a818a20f9e93",
|
||||
"sha256:468100af69b090893bc790a21423fc74f5221b92bba1e49fb83aadcc2774c7b5"
|
||||
"sha256:a012570d3535ec6c4db97e60ef51c2f39f38246429e1455cecc26c633ed81c10",
|
||||
"sha256:c7f45b0417395d3020c98cdc10f942939883018210e29dbfe6fbfc0a74e503ec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.18.5"
|
||||
"version": "==1.18.7"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:0070c5e02b581db40ff5fd1b5e02db90ed88e7e861901894bd78fd998656da68",
|
||||
"sha256:bed34fe7a007180f4208b65515bab1755cdd9fcf2c6720f74ae7ecd2e707f4b7"
|
||||
"sha256:34c8b151a25616ed7791218f6d7780c3a97725fe3ceeaa28085b345a8513af6e",
|
||||
"sha256:dcf399d21170bb899e00d2a693bddcc79e61471fbfead8500a65578700a3190a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.5"
|
||||
"version": "==1.21.7"
|
||||
},
|
||||
"cachetools": {
|
||||
"hashes": [
|
||||
@ -146,22 +146,22 @@
|
||||
},
|
||||
"cbor2": {
|
||||
"hashes": [
|
||||
"sha256:059363ae716c60f6ba29aa61b3d9c57896189c351c4119095f0542aec169e4dc",
|
||||
"sha256:0b80a4a4fca830af3d3cf36b725c31f0a98106e9c2b02004ab73b0ec7f139446",
|
||||
"sha256:0d22b47fb24b384200277fcfb0582c3a3551c413ad51f3bd3ee334caaf79a483",
|
||||
"sha256:3c586a6e328ba5020802346f5e0304f81b982dcafeb51ee4109c9be9cccbc4a0",
|
||||
"sha256:4dd142764607b1a8b5e3e3b474d2b84099e9cbb323596a15ee8db0d78901d95f",
|
||||
"sha256:6f8a7911c2307ee8f8d4940bdcfb8bd21608f14203a83b651fcd7868bce377a5",
|
||||
"sha256:7ecc4e9c548282a5d296d4535244efa69c7f67cda959f28e14929cf1d6af8a97",
|
||||
"sha256:8bc9f5054650d05e6d3e90f6490dcd6ef6c01ad9c1568958a48dde2702824cb1",
|
||||
"sha256:98410520482796a547af2d5ffe11a8a2dc3b9f2124834fa7c12db8264935ed61",
|
||||
"sha256:a7926f7244b08c413f1a4fa71a81aa256771c75bdf1a4fd77308547a2d63dd48",
|
||||
"sha256:ae31d3b5966807fdff6c9e6f894b0aa10474295d9ff8467a8b978a569c8fec47",
|
||||
"sha256:ce6219986385778b1ab7f9b542f160bb4d3558f52975e914a27b774e47016fb7",
|
||||
"sha256:d562b2773e14ee1d65ea5b85351a83a64d4f3fd011bc2b4c70a6e813e78203ce"
|
||||
"sha256:0144ba1f44e4e36f7a8e8408eea72e1af6fc3ee42a704dacd4446307024e5231",
|
||||
"sha256:15102b45dd8b1879b8743159af4538cbf4b3240fe3ebc4e747f6842cd7775888",
|
||||
"sha256:40aa7c9dc9f69c38a2f9954e0adec266b04c55ed3188dc7a0213a92a2054220e",
|
||||
"sha256:4b62aa7a95960d1c382e858c2c4cb24375cde3cae137d11875bb9a4667731011",
|
||||
"sha256:6288b22cd3c0c842db2a4896473512fe83d24fa8ef4bc592d970635a2bb42e0e",
|
||||
"sha256:81676dc7802029299dc168a1240cf1058c1fe5303fbc64598fe14bdb1f8bc076",
|
||||
"sha256:8684c6ffbd35258cb9790ef2722559f585fb971288d6f55ee5efd9ba75dcc81b",
|
||||
"sha256:8ce511337cbac10ccb97093649d6597aacb648ce3198e6afe8b4931fd1cabc61",
|
||||
"sha256:986a8a9a4d3598008ece7241b746261118ef8d7c0efe7e6e9ce8b275f0421646",
|
||||
"sha256:a8bf432f6cb595f50aeb8fed2a4aa3b3f7caa7f135fb57e4378eaa39242feac9",
|
||||
"sha256:ba5e8065ca901ebec7ae390a183f3c13560454b6bd7dd81bf72c320e252b6461",
|
||||
"sha256:d66350d1323460e1e9dcb2f9caa591b60833623f909173b840a0891a245cad83",
|
||||
"sha256:e921d445575fbbe62ae68dc8ff3c6e05b341077fd24c6310c917b96fabe5e64a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==5.4.0"
|
||||
"version": "==5.4.1"
|
||||
},
|
||||
"celery": {
|
||||
"hashes": [
|
||||
@ -446,11 +446,11 @@
|
||||
},
|
||||
"drf-spectacular": {
|
||||
"hashes": [
|
||||
"sha256:6ffbfde7d96a4a2febd19182cc405217e1e86a50280fc739402291c93d1a32b7",
|
||||
"sha256:77593024bb899f69227abedcf87def7851a11c9978f781aa4b385a10f67a38b7"
|
||||
"sha256:f080128c42183fcaed6b9e8e5afcd2e5cd68426b1f80bfc85938f25e62db7fe5",
|
||||
"sha256:fb19aa69fcfcd37b0c9dfb9989c0671e1bb47af332ca2171378c7f840263788c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.17.2"
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"duo-client": {
|
||||
"hashes": [
|
||||
@ -995,10 +995,10 @@
|
||||
},
|
||||
"python-dotenv": {
|
||||
"hashes": [
|
||||
"sha256:dd8fe852847f4fbfadabf6183ddd4c824a9651f02d51714fa075c95561959c7d",
|
||||
"sha256:effaac3c1e58d89b3ccb4d04a40dc7ad6e0275fda25fd75ae9d323e2465e202d"
|
||||
"sha256:aae25dc1ebe97c420f50b81fb0e5c949659af713f31fdb63c749ca68748f34b1",
|
||||
"sha256:f521bc2ac9a8e03c736f62911605c5d83970021e3fa95b37d769e2bbbe9b6172"
|
||||
],
|
||||
"version": "==0.18.0"
|
||||
"version": "==0.19.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
@ -1573,11 +1573,11 @@
|
||||
},
|
||||
"gitpython": {
|
||||
"hashes": [
|
||||
"sha256:17690588e36bd0cee21921ce621fad1e8be45a37afa486ff846fb8efcf53c34c",
|
||||
"sha256:18f4039b96b5567bc4745eb851737ce456a2d499cecd71e84f5c0950e92d0e53"
|
||||
"sha256:b838a895977b45ab6f0cc926a9045c8d1c44e2b653c1fcc39fe91f42c6e8f05b",
|
||||
"sha256:fce760879cd2aebd2991b3542876dc5c4a909b30c9d69dfc488e504a8db37ee8"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.1.19"
|
||||
"version": "==3.1.18"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
@ -1598,7 +1598,7 @@
|
||||
"sha256:eed17b53c3e7912425579853d078a0832820f023191561fcee9d7cae424e0813",
|
||||
"sha256:f65ce5bd4cbc6abdfbe29afc2f0245538ab358c14590912df638033f157d555e"
|
||||
],
|
||||
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
||||
"markers": "python_version < '4' and python_full_version >= '3.6.1'",
|
||||
"version": "==5.9.2"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
@ -1866,14 +1866,6 @@
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.10.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497",
|
||||
"sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342",
|
||||
"sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"
|
||||
],
|
||||
"version": "==3.10.0.0"
|
||||
},
|
||||
"urllib3": {
|
||||
"extras": [
|
||||
"secure"
|
||||
|
@ -2,10 +2,13 @@
|
||||
|
||||
## Supported Versions
|
||||
|
||||
(.x being the latest patch release for each version)
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2021.4.x | :white_check_mark: |
|
||||
| 2021.5.x | :white_check_mark: |
|
||||
| 2021.6.x | :white_check_mark: |
|
||||
| 2021.7.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
|
@ -1,3 +1,3 @@
|
||||
"""authentik"""
|
||||
__version__ = "2021.7.1-rc2"
|
||||
__version__ = "2021.7.3"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
@ -114,23 +114,23 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
},
|
||||
)
|
||||
@action(detail=True, methods=["GET"])
|
||||
# pylint: disable=unused-argument
|
||||
def check_access(self, request: Request, slug: str) -> Response:
|
||||
"""Check access to a single application by slug"""
|
||||
# Don't use self.get_object as that checks for view_application permission
|
||||
# which the user might not have, even if they have access
|
||||
application = get_object_or_404(Application, slug=slug)
|
||||
# If the current user is superuser, they can set `for_user`
|
||||
for_user = self.request.user
|
||||
if self.request.user.is_superuser and "for_user" in request.data:
|
||||
for_user = get_object_or_404(User, pk=request.data.get("for_user"))
|
||||
engine = PolicyEngine(application, for_user, self.request)
|
||||
for_user = request.user
|
||||
if request.user.is_superuser and "for_user" in request.query_params:
|
||||
for_user = get_object_or_404(User, pk=request.query_params.get("for_user"))
|
||||
engine = PolicyEngine(application, for_user, request)
|
||||
engine.use_cache = False
|
||||
engine.build()
|
||||
result = engine.result
|
||||
response = PolicyTestResultSerializer(PolicyResult(False))
|
||||
if result.passing:
|
||||
response = PolicyTestResultSerializer(PolicyResult(True))
|
||||
if self.request.user.is_superuser:
|
||||
if request.user.is_superuser:
|
||||
response = PolicyTestResultSerializer(result)
|
||||
return Response(response.data)
|
||||
|
||||
@ -145,18 +145,20 @@ class ApplicationViewSet(UsedByMixin, ModelViewSet):
|
||||
)
|
||||
def list(self, request: Request) -> Response:
|
||||
"""Custom list method that checks Policy based access instead of guardian"""
|
||||
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
self.paginate_queryset(queryset)
|
||||
|
||||
should_cache = request.GET.get("search", "") == ""
|
||||
|
||||
superuser_full_list = (
|
||||
str(request.GET.get("superuser_full_list", "false")).lower() == "true"
|
||||
)
|
||||
if superuser_full_list and request.user.is_superuser:
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
return super().list(request)
|
||||
|
||||
# To prevent the user from having to double login when prompt is set to login
|
||||
# and the user has just signed it. This session variable is set in the UserLoginStage
|
||||
# and is (quite hackily) removed from the session in applications's API's List method
|
||||
self.request.session.pop(USER_LOGIN_AUTHENTICATED, None)
|
||||
queryset = self._filter_queryset_for_list(self.get_queryset())
|
||||
self.paginate_queryset(queryset)
|
||||
|
||||
allowed_applications = []
|
||||
if not should_cache:
|
||||
|
@ -48,7 +48,7 @@ class TokenViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Token Viewset"""
|
||||
|
||||
lookup_field = "identifier"
|
||||
queryset = Token.filter_not_expired()
|
||||
queryset = Token.objects.all()
|
||||
serializer_class = TokenSerializer
|
||||
search_fields = [
|
||||
"identifier",
|
||||
|
@ -10,6 +10,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_field
|
||||
from guardian.utils import get_anonymous_user
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.fields import CharField, JSONField, SerializerMethodField
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.request import Request
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import (
|
||||
@ -62,12 +63,40 @@ class UserSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class UserSelfSerializer(ModelSerializer):
|
||||
"""User Serializer for information a user can retrieve about themselves and
|
||||
update about themselves"""
|
||||
|
||||
is_superuser = BooleanField(read_only=True)
|
||||
avatar = CharField(read_only=True)
|
||||
groups = ListSerializer(child=GroupSerializer(), read_only=True, source="ak_groups")
|
||||
uid = CharField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = User
|
||||
fields = [
|
||||
"pk",
|
||||
"username",
|
||||
"name",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
"groups",
|
||||
"email",
|
||||
"avatar",
|
||||
"uid",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"is_active": {"read_only": True},
|
||||
}
|
||||
|
||||
|
||||
class SessionUserSerializer(PassiveSerializer):
|
||||
"""Response for the /user/me endpoint, returns the currently active user (as `user` property)
|
||||
and, if this user is being impersonated, the original user in the `original` property."""
|
||||
|
||||
user = UserSerializer()
|
||||
original = UserSerializer(required=False)
|
||||
user = UserSelfSerializer()
|
||||
original = UserSelfSerializer(required=False)
|
||||
|
||||
|
||||
class UserMetricsSerializer(PassiveSerializer):
|
||||
@ -158,12 +187,36 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
data={"user": UserSerializer(request.user).data}
|
||||
)
|
||||
if SESSION_IMPERSONATE_USER in request._request.session:
|
||||
serializer.initial_data["original"] = UserSerializer(
|
||||
serializer.initial_data["original"] = UserSelfSerializer(
|
||||
request._request.session[SESSION_IMPERSONATE_ORIGINAL_USER]
|
||||
).data
|
||||
serializer.is_valid()
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
request=UserSelfSerializer, responses={200: SessionUserSerializer(many=False)}
|
||||
)
|
||||
@action(
|
||||
methods=["PUT"],
|
||||
detail=False,
|
||||
pagination_class=None,
|
||||
filter_backends=[],
|
||||
permission_classes=[IsAuthenticated],
|
||||
)
|
||||
def update_self(self, request: Request) -> Response:
|
||||
"""Allow users to change information on their own profile"""
|
||||
data = UserSelfSerializer(
|
||||
instance=User.objects.get(pk=request.user.pk), data=request.data
|
||||
)
|
||||
if not data.is_valid():
|
||||
return Response(data.errors)
|
||||
new_user = data.save()
|
||||
# If we're impersonating, we need to update that user object
|
||||
# since it caches the full object
|
||||
if SESSION_IMPERSONATE_USER in request.session:
|
||||
request.session[SESSION_IMPERSONATE_USER] = new_user
|
||||
return self.me(request)
|
||||
|
||||
@permission_required("authentik_core.view_user", ["authentik_events.view_event"])
|
||||
@extend_schema(responses={200: UserMetricsSerializer(many=False)})
|
||||
@action(detail=True, pagination_class=None, filter_backends=[])
|
||||
|
@ -438,6 +438,7 @@ class Token(ManagedModel, ExpiringModel):
|
||||
from authentik.events.models import Event, EventAction
|
||||
|
||||
self.key = default_token_key()
|
||||
self.expires = default_token_duration()
|
||||
self.save(*args, **kwargs)
|
||||
Event.new(
|
||||
action=EventAction.SECRET_ROTATE,
|
||||
|
@ -141,11 +141,11 @@ class SourceFlowManager:
|
||||
self._logger.info("denying source because user exists", user=user)
|
||||
return Action.DENY, None
|
||||
# Should never get here as default enroll case is returned above.
|
||||
return Action.DENY, None
|
||||
return Action.DENY, None # pragma: no cover
|
||||
|
||||
def update_connection(
|
||||
self, connection: UserSourceConnection, **kwargs
|
||||
) -> UserSourceConnection:
|
||||
) -> UserSourceConnection: # pragma: no cover
|
||||
"""Optionally make changes to the connection after it is looked up/created."""
|
||||
return connection
|
||||
|
||||
@ -178,7 +178,7 @@ class SourceFlowManager:
|
||||
% {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
return redirect("/")
|
||||
return redirect(reverse("authentik_core:root-redirect"))
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get_stages_to_append(self, flow: Flow) -> list[Stage]:
|
||||
|
160
authentik/core/tests/test_source_flow_manager.py
Normal file
160
authentik/core/tests/test_source_flow_manager.py
Normal file
@ -0,0 +1,160 @@
|
||||
"""Test Source flow_manager"""
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.messages.middleware import MessageMiddleware
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.http.request import HttpRequest
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import SourceUserMatchingModes, User
|
||||
from authentik.core.sources.flow_manager import Action
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.providers.oauth2.generators import generate_client_id
|
||||
from authentik.sources.oauth.models import OAuthSource, UserOAuthSourceConnection
|
||||
from authentik.sources.oauth.views.callback import OAuthSourceFlowManager
|
||||
|
||||
|
||||
class TestSourceFlowManager(TestCase):
|
||||
"""Test Source flow_manager"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.source = OAuthSource.objects.create(name="test")
|
||||
self.factory = RequestFactory()
|
||||
self.identifier = generate_client_id()
|
||||
|
||||
def get_request(self, user: User) -> HttpRequest:
|
||||
"""Helper to create a get request with session and message middleware"""
|
||||
request = self.factory.get("/")
|
||||
request.user = user
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
middleware = MessageMiddleware(dummy_get_response)
|
||||
middleware.process_request(request)
|
||||
request.session.save()
|
||||
return request
|
||||
|
||||
def test_unauthenticated_enroll(self):
|
||||
"""Test un-authenticated user enrolling"""
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_auth(self):
|
||||
"""Test un-authenticated user authenticating"""
|
||||
UserOAuthSourceConnection.objects.create(
|
||||
user=get_anonymous_user(), source=self.source, identifier=self.identifier
|
||||
)
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.AUTH)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_authenticated_link(self):
|
||||
"""Test authenticated user linking"""
|
||||
UserOAuthSourceConnection.objects.create(
|
||||
user=get_anonymous_user(), source=self.source, identifier=self.identifier
|
||||
)
|
||||
user = User.objects.create(username="foo", email="foo@bar.baz")
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(user), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_enroll_email(self):
|
||||
"""Test un-authenticated user enrolling (link on email)"""
|
||||
User.objects.create(username="foo", email="foo@bar.baz")
|
||||
self.source.user_matching_mode = SourceUserMatchingModes.EMAIL_LINK
|
||||
|
||||
# Without email, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
flow_manager.get_flow()
|
||||
# With email
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
self.identifier,
|
||||
{"email": "foo@bar.baz"},
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_enroll_username(self):
|
||||
"""Test un-authenticated user enrolling (link on username)"""
|
||||
User.objects.create(username="foo", email="foo@bar.baz")
|
||||
self.source.user_matching_mode = SourceUserMatchingModes.USERNAME_LINK
|
||||
|
||||
# Without username, deny
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source, self.get_request(AnonymousUser()), self.identifier, {}
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
flow_manager.get_flow()
|
||||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.LINK)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_enroll_username_deny(self):
|
||||
"""Test un-authenticated user enrolling (deny on username)"""
|
||||
User.objects.create(username="foo", email="foo@bar.baz")
|
||||
self.source.user_matching_mode = SourceUserMatchingModes.USERNAME_DENY
|
||||
|
||||
# With non-existent username, enroll
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
self.identifier,
|
||||
{
|
||||
"username": "bar",
|
||||
},
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
flow_manager.get_flow()
|
||||
# With username
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.DENY)
|
||||
flow_manager.get_flow()
|
||||
|
||||
def test_unauthenticated_enroll_link_non_existent(self):
|
||||
"""Test un-authenticated user enrolling (link on username), username doesn't exist"""
|
||||
self.source.user_matching_mode = SourceUserMatchingModes.USERNAME_LINK
|
||||
|
||||
flow_manager = OAuthSourceFlowManager(
|
||||
self.source,
|
||||
self.get_request(AnonymousUser()),
|
||||
self.identifier,
|
||||
{"username": "foo"},
|
||||
)
|
||||
action, _ = flow_manager.get_action()
|
||||
self.assertEqual(action, Action.ENROLL)
|
||||
flow_manager.get_flow()
|
@ -24,8 +24,10 @@ from authentik.events.geo import GEOIP_READER
|
||||
from authentik.events.utils import cleanse_dict, get_user, model_to_dict, sanitize_dict
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.http import get_client_ip
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.policies.models import PolicyBindingModel
|
||||
from authentik.stages.email.utils import TemplateEmailMessage
|
||||
from authentik.tenants.models import Tenant
|
||||
from authentik.tenants.utils import DEFAULT_TENANT
|
||||
|
||||
LOGGER = get_logger("authentik.events")
|
||||
@ -37,7 +39,8 @@ GAUGE_EVENTS = Gauge(
|
||||
|
||||
|
||||
def default_event_duration():
|
||||
"""Default duration an Event is saved"""
|
||||
"""Default duration an Event is saved.
|
||||
This is used as a fallback when no tenant is available"""
|
||||
return now() + timedelta(days=365)
|
||||
|
||||
|
||||
@ -147,7 +150,12 @@ class Event(ExpiringModel):
|
||||
"method": request.method,
|
||||
}
|
||||
if hasattr(request, "tenant"):
|
||||
self.tenant = sanitize_dict(model_to_dict(request.tenant))
|
||||
tenant: Tenant = request.tenant
|
||||
# Because self.created only gets set on save, we can't use it's value here
|
||||
# hence we set self.created to now and then use it
|
||||
self.created = now()
|
||||
self.expires = self.created + timedelta_from_string(tenant.event_retention)
|
||||
self.tenant = sanitize_dict(model_to_dict(tenant))
|
||||
if hasattr(request, "user"):
|
||||
original_user = None
|
||||
if hasattr(request, "session"):
|
||||
|
@ -114,7 +114,7 @@ class MonitoredTask(Task):
|
||||
# For tasks that should only be listed if they failed, set this to False
|
||||
save_on_success: bool
|
||||
|
||||
_result: TaskResult
|
||||
_result: Optional[TaskResult]
|
||||
|
||||
_uid: Optional[str]
|
||||
|
||||
@ -122,7 +122,7 @@ class MonitoredTask(Task):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.save_on_success = True
|
||||
self._uid = None
|
||||
self._result = TaskResult(status=TaskResultStatus.ERROR, messages=[])
|
||||
self._result = None
|
||||
self.result_timeout_hours = 6
|
||||
self.start = default_timer()
|
||||
|
||||
@ -138,25 +138,30 @@ class MonitoredTask(Task):
|
||||
def after_return(
|
||||
self, status, retval, task_id, args: list[Any], kwargs: dict[str, Any], einfo
|
||||
):
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
if self.save_on_success:
|
||||
TaskInfo(
|
||||
task_name=self.__name__,
|
||||
task_description=self.__doc__,
|
||||
start_timestamp=self.start,
|
||||
finish_timestamp=default_timer(),
|
||||
finish_time=datetime.now(),
|
||||
result=self._result,
|
||||
task_call_module=self.__module__,
|
||||
task_call_func=self.__name__,
|
||||
task_call_args=args,
|
||||
task_call_kwargs=kwargs,
|
||||
).save(self.result_timeout_hours)
|
||||
if self._result:
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
if self.save_on_success:
|
||||
TaskInfo(
|
||||
task_name=self.__name__,
|
||||
task_description=self.__doc__,
|
||||
start_timestamp=self.start,
|
||||
finish_timestamp=default_timer(),
|
||||
finish_time=datetime.now(),
|
||||
result=self._result,
|
||||
task_call_module=self.__module__,
|
||||
task_call_func=self.__name__,
|
||||
task_call_args=args,
|
||||
task_call_kwargs=kwargs,
|
||||
).save(self.result_timeout_hours)
|
||||
return super().after_return(status, retval, task_id, args, kwargs, einfo=einfo)
|
||||
|
||||
# pylint: disable=too-many-arguments
|
||||
def on_failure(self, exc, task_id, args, kwargs, einfo):
|
||||
if not self._result:
|
||||
self._result = TaskResult(
|
||||
status=TaskResultStatus.ERROR, messages=[str(exc)]
|
||||
)
|
||||
if not self._result.uid:
|
||||
self._result.uid = self._uid
|
||||
TaskInfo(
|
||||
|
@ -26,7 +26,7 @@ from sentry_sdk import capture_exception
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.models import USER_ATTRIBUTE_DEBUG
|
||||
from authentik.events.models import cleanse_dict
|
||||
from authentik.events.models import Event, EventAction, cleanse_dict
|
||||
from authentik.flows.challenge import (
|
||||
AccessDeniedChallenge,
|
||||
Challenge,
|
||||
@ -52,6 +52,7 @@ from authentik.flows.planner import (
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
from authentik.lib.utils.errors import exception_to_string
|
||||
from authentik.lib.utils.reflection import all_subclasses, class_to_path
|
||||
from authentik.lib.utils.urls import is_url_absolute, redirect_with_qs
|
||||
from authentik.tenants.models import Tenant
|
||||
@ -203,6 +204,18 @@ class FlowExecutorView(APIView):
|
||||
except InvalidStageError as exc:
|
||||
return self.stage_invalid(str(exc))
|
||||
|
||||
def handle_exception(self, exc: Exception) -> HttpResponse:
|
||||
"""Handle exception in stage execution"""
|
||||
if settings.DEBUG or settings.TEST:
|
||||
raise exc
|
||||
capture_exception(exc)
|
||||
self._logger.warning(exc)
|
||||
Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=exception_to_string(exc),
|
||||
).from_http(self.request)
|
||||
return to_stage_response(self.request, FlowErrorResponse(self.request, exc))
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
200: PolymorphicProxySerializer(
|
||||
@ -237,11 +250,7 @@ class FlowExecutorView(APIView):
|
||||
stage_response = self.current_stage_view.get(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
if settings.DEBUG or settings.TEST:
|
||||
raise exc
|
||||
capture_exception(exc)
|
||||
self._logger.warning(exc)
|
||||
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||
return self.handle_exception(exc)
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
@ -278,11 +287,7 @@ class FlowExecutorView(APIView):
|
||||
stage_response = self.current_stage_view.post(request, *args, **kwargs)
|
||||
return to_stage_response(request, stage_response)
|
||||
except Exception as exc: # pylint: disable=broad-except
|
||||
if settings.DEBUG or settings.TEST:
|
||||
raise exc
|
||||
capture_exception(exc)
|
||||
self._logger.warning(exc)
|
||||
return to_stage_response(request, FlowErrorResponse(request, exc))
|
||||
return self.handle_exception(exc)
|
||||
|
||||
def _initiate_plan(self) -> FlowPlan:
|
||||
planner = FlowPlanner(self.flow)
|
||||
@ -317,13 +322,15 @@ class FlowExecutorView(APIView):
|
||||
"""User Successfully passed all stages"""
|
||||
# Since this is wrapped by the ExecutorShell, the next argument is saved in the session
|
||||
# extract the next param before cancel as that cleans it
|
||||
next_param = None
|
||||
if self.plan:
|
||||
next_param = self.plan.context.get(PLAN_CONTEXT_REDIRECT)
|
||||
if not next_param:
|
||||
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:root-redirect"
|
||||
)
|
||||
if self.plan and PLAN_CONTEXT_REDIRECT in self.plan.context:
|
||||
# The context `redirect` variable can only be set by
|
||||
# an expression policy or authentik itself, so we don't
|
||||
# check if its an absolute URL or a relative one
|
||||
self.cancel()
|
||||
return redirect(self.plan.context.get(PLAN_CONTEXT_REDIRECT))
|
||||
next_param = self.request.session.get(SESSION_KEY_GET, {}).get(
|
||||
NEXT_ARG_NAME, "authentik_core:root-redirect"
|
||||
)
|
||||
self.cancel()
|
||||
return to_stage_response(self.request, redirect_with_qs(next_param))
|
||||
|
||||
|
@ -5,12 +5,12 @@ from django.core.exceptions import ValidationError
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
ALLOWED_KEYS = (
|
||||
"days",
|
||||
"seconds",
|
||||
"microseconds",
|
||||
"milliseconds",
|
||||
"seconds",
|
||||
"minutes",
|
||||
"hours",
|
||||
"days",
|
||||
"weeks",
|
||||
)
|
||||
|
||||
|
@ -22,7 +22,7 @@ def redirect_with_qs(view: str, get_query_set=None, **kwargs) -> HttpResponse:
|
||||
except NoReverseMatch:
|
||||
if not is_url_absolute(view):
|
||||
return redirect(view)
|
||||
LOGGER.debug("redirect target is not a valid view", view=view)
|
||||
LOGGER.warning("redirect target is not a valid view", view=view)
|
||||
raise
|
||||
else:
|
||||
if get_query_set:
|
||||
|
@ -28,6 +28,7 @@ from authentik.outposts.models import (
|
||||
OutpostServiceConnection,
|
||||
OutpostState,
|
||||
OutpostType,
|
||||
ServiceConnectionInvalid,
|
||||
)
|
||||
from authentik.providers.ldap.controllers.docker import LDAPDockerController
|
||||
from authentik.providers.ldap.controllers.kubernetes import LDAPKubernetesController
|
||||
@ -114,7 +115,7 @@ def outpost_controller(
|
||||
for log in logs:
|
||||
LOGGER.debug(log)
|
||||
LOGGER.debug("-----------------Outpost Controller logs end-------------------")
|
||||
except ControllerException as exc:
|
||||
except (ControllerException, ServiceConnectionInvalid) as exc:
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
else:
|
||||
self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, logs))
|
||||
|
54
authentik/providers/oauth2/tests/test_jwks.py
Normal file
54
authentik/providers/oauth2/tests/test_jwks.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""JWKS tests"""
|
||||
import json
|
||||
|
||||
from django.test import RequestFactory
|
||||
from django.urls.base import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.providers.oauth2.models import OAuth2Provider
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
||||
|
||||
class TestJWKS(OAuthTestCase):
|
||||
"""Test JWKS view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_rs256(self):
|
||||
"""Test JWKS request with RS256"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:jwks", kwargs={"application_slug": app.slug}
|
||||
)
|
||||
)
|
||||
body = json.loads(force_str(response.content))
|
||||
self.assertEqual(len(body["keys"]), 1)
|
||||
|
||||
def test_hs256(self):
|
||||
"""Test JWKS request with HS256"""
|
||||
provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id="test",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="http://local.invalid",
|
||||
)
|
||||
app = Application.objects.create(name="test", slug="test", provider=provider)
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"authentik_providers_oauth2:jwks", kwargs={"application_slug": app.slug}
|
||||
)
|
||||
)
|
||||
self.assertJSONEqual(force_str(response.content), {})
|
@ -30,7 +30,7 @@ class JWKSView(View):
|
||||
|
||||
response_data = {}
|
||||
|
||||
if provider.jwt_alg == JWTAlgorithms.RS256:
|
||||
if provider.jwt_alg == JWTAlgorithms.RS256 and provider.rsa_key:
|
||||
public_key: RSAPublicKey = provider.rsa_key.private_key.public_key()
|
||||
public_numbers = public_key.public_numbers()
|
||||
response_data["keys"] = [
|
||||
|
@ -126,7 +126,15 @@ class TokenParams:
|
||||
LOGGER.warning("Missing authorization code")
|
||||
raise TokenError("invalid_grant")
|
||||
|
||||
if self.redirect_uri not in self.provider.redirect_uris.split():
|
||||
allowed_redirect_urls = self.provider.redirect_uris.split()
|
||||
if len(allowed_redirect_urls) < 1:
|
||||
LOGGER.warning(
|
||||
"Provider has no allowed redirect_uri set, allowing all.",
|
||||
allow=self.redirect_uri.lower(),
|
||||
)
|
||||
elif self.redirect_uri.lower() not in [
|
||||
x.lower() for x in allowed_redirect_urls
|
||||
]:
|
||||
LOGGER.warning(
|
||||
"Invalid redirect uri",
|
||||
uri=self.redirect_uri,
|
||||
|
@ -60,12 +60,12 @@ class IngressReconciler(KubernetesObjectReconciler[NetworkingV1beta1Ingress]):
|
||||
expected_hosts.sort()
|
||||
expected_hosts_tls.sort()
|
||||
|
||||
have_hosts = [rule.host for rule in reference.spec.rules]
|
||||
have_hosts = [rule.host for rule in current.spec.rules]
|
||||
have_hosts.sort()
|
||||
|
||||
have_hosts_tls = []
|
||||
for tls_config in reference.spec.tls:
|
||||
if tls_config:
|
||||
for tls_config in current.spec.tls:
|
||||
if tls_config and tls_config.hosts:
|
||||
have_hosts_tls += tls_config.hosts
|
||||
have_hosts_tls.sort()
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
from xml.etree.ElementTree import ParseError # nosec
|
||||
|
||||
from defusedxml.ElementTree import fromstring
|
||||
from django.http.response import HttpResponse
|
||||
from django.http.response import Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@ -116,7 +116,10 @@ class SAMLProviderViewSet(UsedByMixin, ModelViewSet):
|
||||
def metadata(self, request: Request, pk: int) -> Response:
|
||||
"""Return metadata as XML string"""
|
||||
# We don't use self.get_object() on purpose as this view is un-authenticated
|
||||
provider = get_object_or_404(SAMLProvider, pk=pk)
|
||||
try:
|
||||
provider = get_object_or_404(SAMLProvider, pk=pk)
|
||||
except ValueError:
|
||||
raise Http404
|
||||
try:
|
||||
metadata = MetadataProcessor(provider, request).build_entity_descriptor()
|
||||
if "download" in request._request.GET:
|
||||
|
@ -163,7 +163,7 @@ class AssertionProcessor:
|
||||
provider=self.provider,
|
||||
)
|
||||
if value is not None:
|
||||
name_id.text = value
|
||||
name_id.text = str(value)
|
||||
return name_id
|
||||
except PropertyMappingExpressionException as exc:
|
||||
Event.new(
|
||||
|
@ -134,10 +134,18 @@ class ServiceProviderMetadataParser:
|
||||
# For now we'll only look at the first descriptor.
|
||||
# Even if multiple descriptors exist, we can only configure one
|
||||
descriptor = sp_sso_descriptors[0]
|
||||
auth_n_request_signed = (
|
||||
descriptor.attrib["AuthnRequestsSigned"].lower() == "true"
|
||||
)
|
||||
assertion_signed = descriptor.attrib["WantAssertionsSigned"].lower() == "true"
|
||||
|
||||
auth_n_request_signed = False
|
||||
if "AuthnRequestsSigned" in descriptor.attrib:
|
||||
auth_n_request_signed = (
|
||||
descriptor.attrib["AuthnRequestsSigned"].lower() == "true"
|
||||
)
|
||||
|
||||
assertion_signed = False
|
||||
if "WantAssertionsSigned" in descriptor.attrib:
|
||||
assertion_signed = (
|
||||
descriptor.attrib["WantAssertionsSigned"].lower() == "true"
|
||||
)
|
||||
|
||||
acs_services = descriptor.findall(
|
||||
f"{{{NS_SAML_METADATA}}}AssertionConsumerService"
|
||||
|
@ -20,6 +20,7 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
|
||||
def test_metadata(self):
|
||||
"""Test metadata export (normal)"""
|
||||
self.client.logout()
|
||||
provider = SAMLProvider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=Flow.objects.get(
|
||||
@ -34,6 +35,7 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
|
||||
def test_metadata_download(self):
|
||||
"""Test metadata export (download)"""
|
||||
self.client.logout()
|
||||
provider = SAMLProvider.objects.create(
|
||||
name="test",
|
||||
authorization_flow=Flow.objects.get(
|
||||
@ -50,6 +52,7 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
|
||||
def test_metadata_invalid(self):
|
||||
"""Test metadata export (invalid)"""
|
||||
self.client.logout()
|
||||
# Provider without application
|
||||
provider = SAMLProvider.objects.create(
|
||||
name="test",
|
||||
@ -61,6 +64,10 @@ class TestSAMLProviderAPI(APITestCase):
|
||||
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": provider.pk}),
|
||||
)
|
||||
self.assertEqual(200, response.status_code)
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:samlprovider-metadata", kwargs={"pk": "abc"}),
|
||||
)
|
||||
self.assertEqual(404, response.status_code)
|
||||
|
||||
def test_import_success(self):
|
||||
"""Test metadata import (success case)"""
|
||||
|
@ -105,15 +105,17 @@ class LDAPPasswordChanger:
|
||||
if len(user_attributes["sAMAccountName"]) >= 3:
|
||||
if password.lower() in user_attributes["sAMAccountName"].lower():
|
||||
return False
|
||||
display_name_tokens = split(
|
||||
RE_DISPLAYNAME_SEPARATORS, user_attributes["displayName"]
|
||||
)
|
||||
for token in display_name_tokens:
|
||||
# Ignore tokens under 3 chars
|
||||
if len(token) < 3:
|
||||
continue
|
||||
if token.lower() in password.lower():
|
||||
return False
|
||||
# No display name set, can't check any further
|
||||
if len(user_attributes["displayName"]) < 1:
|
||||
return True
|
||||
for display_name in user_attributes["displayName"]:
|
||||
display_name_tokens = split(RE_DISPLAYNAME_SEPARATORS, display_name)
|
||||
for token in display_name_tokens:
|
||||
# Ignore tokens under 3 chars
|
||||
if len(token) < 3:
|
||||
continue
|
||||
if token.lower() in password.lower():
|
||||
return False
|
||||
return True
|
||||
|
||||
def ad_password_complexity(
|
||||
|
10
authentik/sources/plex/settings.py
Normal file
10
authentik/sources/plex/settings.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Plex source settings"""
|
||||
from celery.schedules import crontab
|
||||
|
||||
CELERY_BEAT_SCHEDULE = {
|
||||
"check_plex_token": {
|
||||
"task": "authentik.sources.plex.tasks.check_plex_token_all",
|
||||
"schedule": crontab(minute="31", hour="*/3"),
|
||||
"options": {"queue": "authentik_scheduled"},
|
||||
},
|
||||
}
|
43
authentik/sources/plex/tasks.py
Normal file
43
authentik/sources/plex/tasks.py
Normal file
@ -0,0 +1,43 @@
|
||||
"""Plex tasks"""
|
||||
from requests import RequestException
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.events.monitored_tasks import MonitoredTask, TaskResult, TaskResultStatus
|
||||
from authentik.root.celery import CELERY_APP
|
||||
from authentik.sources.plex.models import PlexSource
|
||||
from authentik.sources.plex.plex import PlexAuth
|
||||
|
||||
|
||||
@CELERY_APP.task()
|
||||
def check_plex_token_all():
|
||||
"""Check plex token for all plex sources"""
|
||||
for source in PlexSource.objects.all():
|
||||
check_plex_token.delay(source.slug)
|
||||
|
||||
|
||||
@CELERY_APP.task(bind=True, base=MonitoredTask)
|
||||
def check_plex_token(self: MonitoredTask, source_slug: int):
|
||||
"""Check the validity of a Plex source."""
|
||||
sources = PlexSource.objects.filter(slug=source_slug)
|
||||
if not sources.exists():
|
||||
return
|
||||
source: PlexSource = sources.first()
|
||||
self.set_uid(source.slug)
|
||||
auth = PlexAuth(source, source.plex_token)
|
||||
try:
|
||||
auth.get_user_info()
|
||||
self.set_status(
|
||||
TaskResult(TaskResultStatus.SUCCESSFUL, ["Plex token is valid."])
|
||||
)
|
||||
except RequestException as exc:
|
||||
self.set_status(
|
||||
TaskResult(
|
||||
TaskResultStatus.ERROR,
|
||||
["Plex token is invalid/an error occurred:", str(exc)],
|
||||
)
|
||||
)
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Plex token invalid, please re-authenticate source.\n{str(exc)}",
|
||||
source=source,
|
||||
).save()
|
@ -1,10 +1,13 @@
|
||||
"""plex Source tests"""
|
||||
from django.test import TestCase
|
||||
from requests.exceptions import RequestException
|
||||
from requests_mock import Mocker
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.sources.plex.models import PlexSource
|
||||
from authentik.sources.plex.plex import PlexAuth
|
||||
from authentik.sources.plex.tasks import check_plex_token_all
|
||||
|
||||
USER_INFO_RESPONSE = {
|
||||
"id": 1234123419,
|
||||
@ -62,3 +65,18 @@ class TestPlexSource(TestCase):
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/resources", json=RESOURCES_RESPONSE)
|
||||
self.assertTrue(api.check_server_overlap())
|
||||
|
||||
def test_check_task(self):
|
||||
"""Test token check task"""
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/user", json=USER_INFO_RESPONSE)
|
||||
check_plex_token_all()
|
||||
self.assertFalse(
|
||||
Event.objects.filter(action=EventAction.CONFIGURATION_ERROR).exists()
|
||||
)
|
||||
with Mocker() as mocker:
|
||||
mocker.get("https://plex.tv/api/v2/user", exc=RequestException())
|
||||
check_plex_token_all()
|
||||
self.assertTrue(
|
||||
Event.objects.filter(action=EventAction.CONFIGURATION_ERROR).exists()
|
||||
)
|
||||
|
@ -67,10 +67,15 @@ class EmailStageView(ChallengeStageView):
|
||||
"user": pending_user,
|
||||
"identifier": f"ak-email-stage-{current_stage.name}-{pending_user}",
|
||||
}
|
||||
tokens = Token.filter_not_expired(**token_filters)
|
||||
# Don't check for validity here, we only care if the token exists
|
||||
tokens = Token.objects.filter(**token_filters)
|
||||
if not tokens.exists():
|
||||
return Token.objects.create(expires=now() + valid_delta, **token_filters)
|
||||
return tokens.first()
|
||||
token = tokens.first()
|
||||
# Check if token is expired and rotate key if so
|
||||
if token.is_expired:
|
||||
token.expire_action()
|
||||
return token
|
||||
|
||||
def send_email(self):
|
||||
"""Helper function that sends the actual email. Implies that you've
|
||||
|
@ -91,7 +91,7 @@ def send_mail(
|
||||
messages=["Successfully sent Mail."],
|
||||
)
|
||||
)
|
||||
except (SMTPException, ConnectionError) as exc:
|
||||
except (SMTPException, ConnectionError, OSError) as exc:
|
||||
LOGGER.debug("Error sending email, retrying...", exc=exc)
|
||||
self.set_status(TaskResult(TaskResultStatus.ERROR).with_error(exc))
|
||||
raise exc
|
||||
|
@ -1,18 +1,23 @@
|
||||
"""invitation stage logic"""
|
||||
from copy import deepcopy
|
||||
from typing import Optional
|
||||
|
||||
from deepmerge import always_merger
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.http.response import HttpResponseBadRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.models import in_memory_stage
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views import SESSION_KEY_GET
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
from authentik.stages.invitation.signals import invitation_used
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
|
||||
LOGGER = get_logger()
|
||||
INVITATION_TOKEN_KEY = "token" # nosec
|
||||
INVITATION_IN_EFFECT = "invitation_in_effect"
|
||||
INVITATION = "invitation"
|
||||
|
||||
|
||||
class InvitationStageView(StageView):
|
||||
@ -39,9 +44,37 @@ class InvitationStageView(StageView):
|
||||
return self.executor.stage_invalid()
|
||||
|
||||
invite: Invitation = get_object_or_404(Invitation, pk=token)
|
||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = deepcopy(invite.fixed_data)
|
||||
self.executor.plan.context[INVITATION_IN_EFFECT] = True
|
||||
self.executor.plan.context[INVITATION] = invite
|
||||
|
||||
context = {}
|
||||
always_merger.merge(
|
||||
context, self.executor.plan.context.get(PLAN_CONTEXT_PROMPT, {})
|
||||
)
|
||||
always_merger.merge(context, invite.fixed_data)
|
||||
self.executor.plan.context[PLAN_CONTEXT_PROMPT] = context
|
||||
|
||||
invitation_used.send(sender=self, request=request, invitation=invite)
|
||||
if invite.single_use:
|
||||
invite.delete()
|
||||
self.executor.plan.append_stage(in_memory_stage(InvitationFinalStageView))
|
||||
return self.executor.stage_ok()
|
||||
|
||||
|
||||
class InvitationFinalStageView(StageView):
|
||||
"""Final stage which is injected by invitation stage. Deletes
|
||||
the used invitation."""
|
||||
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Call get as this request may be called with post"""
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Delete invitation if single_use is active"""
|
||||
invitation: Invitation = self.executor.plan.context.get(INVITATION, None)
|
||||
if not invitation:
|
||||
LOGGER.warning("InvitationFinalStageView stage called without invitation")
|
||||
return HttpResponseBadRequest
|
||||
if not invitation.single_use:
|
||||
return self.executor.stage_ok()
|
||||
invitation.delete()
|
||||
return self.executor.stage_ok()
|
||||
|
@ -156,11 +156,13 @@ class TestUserLoginStage(TestCase):
|
||||
base_url = reverse(
|
||||
"authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||
)
|
||||
response = self.client.get(base_url)
|
||||
response = self.client.get(base_url, follow=True)
|
||||
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
self.assertEqual(plan.context[PLAN_CONTEXT_PROMPT], data)
|
||||
self.assertEqual(
|
||||
plan.context[PLAN_CONTEXT_PROMPT], data | plan.context[PLAN_CONTEXT_PROMPT]
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertJSONEqual(
|
||||
|
@ -38,6 +38,7 @@ class TenantSerializer(ModelSerializer):
|
||||
"flow_invalidation",
|
||||
"flow_recovery",
|
||||
"flow_unenrollment",
|
||||
"event_retention",
|
||||
]
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@ from typing import Callable
|
||||
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from sentry_sdk.api import set_tag
|
||||
|
||||
from authentik.tenants.utils import get_tenant_for_request
|
||||
|
||||
@ -19,4 +20,6 @@ class TenantMiddleware:
|
||||
if not hasattr(request, "tenant"):
|
||||
tenant = get_tenant_for_request(request)
|
||||
setattr(request, "tenant", tenant)
|
||||
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
|
||||
set_tag("authentik.tenant_domain", tenant.domain)
|
||||
return self.get_response(request)
|
||||
|
24
authentik/tenants/migrations/0004_tenant_event_retention.py
Normal file
24
authentik/tenants/migrations/0004_tenant_event_retention.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.2.5 on 2021-07-24 17:06
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
import authentik.lib.utils.time
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_tenants", "0003_tenant_branding_favicon"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="tenant",
|
||||
name="event_retention",
|
||||
field=models.TextField(
|
||||
default="days=365",
|
||||
help_text="Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).",
|
||||
validators=[authentik.lib.utils.time.timedelta_string_validator],
|
||||
),
|
||||
),
|
||||
]
|
@ -5,6 +5,7 @@ from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.lib.utils.time import timedelta_string_validator
|
||||
|
||||
|
||||
class Tenant(models.Model):
|
||||
@ -22,6 +23,7 @@ class Tenant(models.Model):
|
||||
)
|
||||
|
||||
branding_title = models.TextField(default="authentik")
|
||||
|
||||
branding_logo = models.TextField(
|
||||
default="/static/dist/assets/icons/icon_left_brand.svg"
|
||||
)
|
||||
@ -40,6 +42,17 @@ class Tenant(models.Model):
|
||||
Flow, null=True, on_delete=models.SET_NULL, related_name="tenant_unenrollment"
|
||||
)
|
||||
|
||||
event_retention = models.TextField(
|
||||
default="days=365",
|
||||
validators=[timedelta_string_validator],
|
||||
help_text=_(
|
||||
(
|
||||
"Events will be deleted after this duration."
|
||||
"(Format: weeks=3;days=2;hours=3,seconds=2)."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.default:
|
||||
return "Default tenant"
|
||||
|
@ -1,9 +1,12 @@
|
||||
"""Test tenants"""
|
||||
from django.test import TestCase
|
||||
from django.test.client import RequestFactory
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.tenants.models import Tenant
|
||||
|
||||
|
||||
@ -57,3 +60,28 @@ class TestTenants(TestCase):
|
||||
"ui_footer_links": CONFIG.y("footer_links"),
|
||||
},
|
||||
)
|
||||
|
||||
def test_event_retention(self):
|
||||
"""Test tenant's event retention"""
|
||||
tenant = Tenant.objects.create(
|
||||
domain="foo",
|
||||
default=True,
|
||||
branding_title="custom",
|
||||
event_retention="weeks=3",
|
||||
)
|
||||
factory = RequestFactory()
|
||||
request = factory.get("/")
|
||||
request.tenant = tenant
|
||||
event = Event.new(
|
||||
action=EventAction.SYSTEM_EXCEPTION, message="test"
|
||||
).from_http(request)
|
||||
self.assertEqual(
|
||||
event.expires.day, (event.created + timedelta_from_string("weeks=3")).day
|
||||
)
|
||||
self.assertEqual(
|
||||
event.expires.month,
|
||||
(event.created + timedelta_from_string("weeks=3")).month,
|
||||
)
|
||||
self.assertEqual(
|
||||
event.expires.year, (event.created + timedelta_from_string("weeks=3")).year
|
||||
)
|
||||
|
@ -21,7 +21,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1-rc2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.3}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -44,7 +44,7 @@ services:
|
||||
- "0.0.0.0:9000:9000"
|
||||
- "0.0.0.0:9443:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1-rc2}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.3}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
networks:
|
||||
|
@ -17,4 +17,4 @@ func OutpostUserAgent() string {
|
||||
return fmt.Sprintf("authentik-outpost@%s (%s)", VERSION, BUILD())
|
||||
}
|
||||
|
||||
const VERSION = "2021.7.1-rc2"
|
||||
const VERSION = "2021.7.3"
|
||||
|
@ -116,7 +116,7 @@ func (ac *APIController) startWSHealth() {
|
||||
|
||||
func (ac *APIController) startIntervalUpdater() {
|
||||
logger := ac.logger.WithField("loop", "interval-updater")
|
||||
ticker := time.NewTicker(time.Second * 150)
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
for ; true; <-ticker.C {
|
||||
err := ac.Server.Refresh()
|
||||
if err != nil {
|
||||
|
@ -75,7 +75,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
||||
pi.boundUsersMutex.Lock()
|
||||
cs := pi.SearchAccessCheck(userInfo.User)
|
||||
pi.boundUsers[req.BindDN] = UserFlags{
|
||||
UserInfo: userInfo.User,
|
||||
UserPk: userInfo.User.Pk,
|
||||
CanSearch: cs != nil,
|
||||
}
|
||||
if pi.boundUsers[req.BindDN].CanSearch {
|
||||
@ -88,7 +88,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
||||
}
|
||||
|
||||
// SearchAccessCheck Check if the current user is allowed to search
|
||||
func (pi *ProviderInstance) SearchAccessCheck(user api.User) *string {
|
||||
func (pi *ProviderInstance) SearchAccessCheck(user api.UserSelf) *string {
|
||||
for _, group := range user.Groups {
|
||||
for _, allowedGroup := range pi.searchAllowedGroups {
|
||||
pi.log.WithField("userGroup", group.Pk).WithField("allowedGroup", allowedGroup).Trace("Checking search access")
|
||||
|
@ -11,9 +11,17 @@ import (
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func (pi *ProviderInstance) SearchMe(user api.User) (ldap.ServerSearchResult, error) {
|
||||
func (pi *ProviderInstance) SearchMe(req SearchRequest, f UserFlags) (ldap.ServerSearchResult, error) {
|
||||
if f.UserInfo == nil {
|
||||
u, _, err := pi.s.ac.Client.CoreApi.CoreUsersRetrieve(req.ctx, f.UserInfo.Pk).Execute()
|
||||
if err != nil {
|
||||
req.log.WithError(err).Warning("Failed to get user info")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Failed to get userinfo")
|
||||
}
|
||||
f.UserInfo = &u
|
||||
}
|
||||
entries := make([]*ldap.Entry, 1)
|
||||
entries[0] = pi.UserEntry(user)
|
||||
entries[0] = pi.UserEntry(*f.UserInfo)
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
@ -42,7 +50,7 @@ func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult,
|
||||
}
|
||||
if !flags.CanSearch {
|
||||
pi.log.Debug("User can't search, showing info about user")
|
||||
return pi.SearchMe(flags.UserInfo)
|
||||
return pi.SearchMe(req, flags)
|
||||
}
|
||||
accsp.Finish()
|
||||
|
||||
|
@ -39,7 +39,8 @@ type ProviderInstance struct {
|
||||
}
|
||||
|
||||
type UserFlags struct {
|
||||
UserInfo api.User
|
||||
UserInfo *api.User
|
||||
UserPk int32
|
||||
CanSearch bool
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,8 @@ logconfig_dict = {
|
||||
if SERVICE_HOST_ENV_NAME in os.environ:
|
||||
workers = 2
|
||||
else:
|
||||
workers = int(os.environ.get("WORKERS", cpu_count() * 2 + 1))
|
||||
default_workers = max(cpu_count() * 0.25, 1) + 1 # Minimum of 2 workers
|
||||
workers = int(os.environ.get("WORKERS", default_workers))
|
||||
threads = 4
|
||||
|
||||
warnings.simplefilter("once")
|
||||
|
123
schema.yml
123
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2021.7.1-rc2
|
||||
version: 2021.7.3
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
@ -3185,6 +3185,38 @@ paths:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
/api/v2beta/core/users/update_self/:
|
||||
put:
|
||||
operationId: core_users_update_self_update
|
||||
description: Allow users to change information on their own profile
|
||||
tags:
|
||||
- core
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSelfRequest'
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSelfRequest'
|
||||
multipart/form-data:
|
||||
schema:
|
||||
$ref: '#/components/schemas/UserSelfRequest'
|
||||
required: true
|
||||
security:
|
||||
- authentik: []
|
||||
- cookieAuth: []
|
||||
responses:
|
||||
'200':
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SessionUser'
|
||||
description: ''
|
||||
'400':
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
'403':
|
||||
$ref: '#/components/schemas/GenericError'
|
||||
/api/v2beta/crypto/certificatekeypairs/:
|
||||
get:
|
||||
operationId: crypto_certificatekeypairs_list
|
||||
@ -25902,6 +25934,9 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
event_retention:
|
||||
type: string
|
||||
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
|
||||
PatchedTokenRequest:
|
||||
type: object
|
||||
description: Token Serializer
|
||||
@ -27574,9 +27609,9 @@ components:
|
||||
and, if this user is being impersonated, the original user in the `original` property.
|
||||
properties:
|
||||
user:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: '#/components/schemas/UserSelf'
|
||||
original:
|
||||
$ref: '#/components/schemas/User'
|
||||
$ref: '#/components/schemas/UserSelf'
|
||||
required:
|
||||
- user
|
||||
SetIconRequest:
|
||||
@ -27977,6 +28012,9 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
event_retention:
|
||||
type: string
|
||||
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
|
||||
required:
|
||||
- domain
|
||||
- tenant_uuid
|
||||
@ -28012,6 +28050,9 @@ components:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
event_retention:
|
||||
type: string
|
||||
description: 'Events will be deleted after this duration.(Format: weeks=3;days=2;hours=3,seconds=2).'
|
||||
required:
|
||||
- domain
|
||||
Token:
|
||||
@ -28469,6 +28510,82 @@ components:
|
||||
required:
|
||||
- name
|
||||
- username
|
||||
UserSelf:
|
||||
type: object
|
||||
description: |-
|
||||
User Serializer for information a user can retrieve about themselves and
|
||||
update about themselves
|
||||
properties:
|
||||
pk:
|
||||
type: integer
|
||||
readOnly: true
|
||||
title: ID
|
||||
username:
|
||||
type: string
|
||||
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
||||
only.
|
||||
pattern: ^[\w.@+-]+$
|
||||
maxLength: 150
|
||||
name:
|
||||
type: string
|
||||
description: User's display name.
|
||||
is_active:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
title: Active
|
||||
description: Designates whether this user should be treated as active. Unselect
|
||||
this instead of deleting accounts.
|
||||
is_superuser:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Group'
|
||||
readOnly: true
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
title: Email address
|
||||
maxLength: 254
|
||||
avatar:
|
||||
type: string
|
||||
readOnly: true
|
||||
uid:
|
||||
type: string
|
||||
readOnly: true
|
||||
required:
|
||||
- avatar
|
||||
- groups
|
||||
- is_active
|
||||
- is_superuser
|
||||
- name
|
||||
- pk
|
||||
- uid
|
||||
- username
|
||||
UserSelfRequest:
|
||||
type: object
|
||||
description: |-
|
||||
User Serializer for information a user can retrieve about themselves and
|
||||
update about themselves
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
||||
only.
|
||||
pattern: ^[\w.@+-]+$
|
||||
maxLength: 150
|
||||
name:
|
||||
type: string
|
||||
description: User's display name.
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
title: Email address
|
||||
maxLength: 254
|
||||
required:
|
||||
- name
|
||||
- username
|
||||
UserSetting:
|
||||
type: object
|
||||
description: Serializer for User settings for stages and sources
|
||||
|
@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome:3.141
|
||||
image: selenium/standalone-chrome:3.141.59-20210713
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
@ -2,7 +2,7 @@ version: '3.7'
|
||||
|
||||
services:
|
||||
chrome:
|
||||
image: selenium/standalone-chrome-debug:3.141
|
||||
image: selenium/standalone-chrome-debug:3.141.59-20210713
|
||||
volumes:
|
||||
- /dev/shm:/dev/shm
|
||||
network_mode: host
|
||||
|
@ -1,20 +1,36 @@
|
||||
"""Test Controllers"""
|
||||
from typing import Optional
|
||||
|
||||
import yaml
|
||||
from django.test import TestCase
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.outposts.controllers.kubernetes import KubernetesController
|
||||
from authentik.outposts.models import KubernetesServiceConnection, Outpost, OutpostType
|
||||
from authentik.outposts.tasks import outpost_local_connection
|
||||
from authentik.providers.proxy.controllers.k8s.ingress import IngressReconciler
|
||||
from authentik.providers.proxy.controllers.kubernetes import ProxyKubernetesController
|
||||
from authentik.providers.proxy.models import ProxyProvider
|
||||
from authentik.providers.proxy.models import ProxyMode, ProxyProvider
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class TestProxyKubernetes(TestCase):
|
||||
"""Test Controllers"""
|
||||
|
||||
controller: Optional[KubernetesController]
|
||||
|
||||
def setUp(self):
|
||||
# Ensure that local connection have been created
|
||||
outpost_local_connection()
|
||||
self.controller = None
|
||||
|
||||
def tearDown(self) -> None:
|
||||
if self.controller:
|
||||
for log in self.controller.down_with_logs():
|
||||
LOGGER.info(log)
|
||||
return super().tearDown()
|
||||
|
||||
def test_kubernetes_controller_static(self):
|
||||
"""Test Kubernetes Controller"""
|
||||
@ -33,18 +49,26 @@ class TestProxyKubernetes(TestCase):
|
||||
outpost.providers.add(provider)
|
||||
outpost.save()
|
||||
|
||||
controller = ProxyKubernetesController(outpost, service_connection)
|
||||
manifest = controller.get_static_deployment()
|
||||
self.controller = ProxyKubernetesController(outpost, service_connection)
|
||||
manifest = self.controller.get_static_deployment()
|
||||
self.assertEqual(len(list(yaml.load_all(manifest, Loader=yaml.SafeLoader))), 4)
|
||||
|
||||
def test_kubernetes_controller_deploy(self):
|
||||
"""Test Kubernetes Controller"""
|
||||
def test_kubernetes_controller_ingress(self):
|
||||
"""Test Kubernetes Controller's Ingress"""
|
||||
provider: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test",
|
||||
internal_host="http://localhost",
|
||||
external_host="http://localhost",
|
||||
external_host="https://localhost",
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
provider2: ProxyProvider = ProxyProvider.objects.create(
|
||||
name="test2",
|
||||
internal_host="http://otherhost",
|
||||
external_host="https://otherhost",
|
||||
mode=ProxyMode.FORWARD_SINGLE,
|
||||
authorization_flow=Flow.objects.first(),
|
||||
)
|
||||
|
||||
service_connection = KubernetesServiceConnection.objects.first()
|
||||
outpost: Outpost = Outpost.objects.create(
|
||||
name="test",
|
||||
@ -52,8 +76,19 @@ class TestProxyKubernetes(TestCase):
|
||||
service_connection=service_connection,
|
||||
)
|
||||
outpost.providers.add(provider)
|
||||
outpost.save()
|
||||
|
||||
controller = ProxyKubernetesController(outpost, service_connection)
|
||||
controller.up()
|
||||
controller.down()
|
||||
self.controller = ProxyKubernetesController(outpost, service_connection)
|
||||
|
||||
ingress_rec = IngressReconciler(self.controller)
|
||||
ingress = ingress_rec.retrieve()
|
||||
|
||||
self.assertEqual(len(ingress.spec.rules), 1)
|
||||
self.assertEqual(ingress.spec.rules[0].host, "localhost")
|
||||
|
||||
# add provider, check again
|
||||
outpost.providers.add(provider2)
|
||||
ingress = ingress_rec.retrieve()
|
||||
|
||||
self.assertEqual(len(ingress.spec.rules), 2)
|
||||
self.assertEqual(ingress.spec.rules[0].host, "localhost")
|
||||
self.assertEqual(ingress.spec.rules[1].host, "otherhost")
|
||||
|
168
web/package-lock.json
generated
168
web/package-lock.json
generated
@ -29,13 +29,13 @@
|
||||
"@types/chart.js": "^2.9.34",
|
||||
"@types/codemirror": "5.60.2",
|
||||
"@types/grecaptcha": "^3.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.4",
|
||||
"@typescript-eslint/parser": "^4.28.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.5",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"@webcomponents/webcomponentsjs": "^2.5.0",
|
||||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.4.1",
|
||||
"chart.js": "^3.5.0",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.62.2",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
@ -48,7 +48,7 @@
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.53.3",
|
||||
"rollup": "^2.54.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
@ -2606,12 +2606,12 @@
|
||||
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz",
|
||||
"integrity": "sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz",
|
||||
"integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/experimental-utils": "4.28.4",
|
||||
"@typescript-eslint/scope-manager": "4.28.4",
|
||||
"@typescript-eslint/experimental-utils": "4.28.5",
|
||||
"@typescript-eslint/scope-manager": "4.28.5",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.1.0",
|
||||
@ -2636,14 +2636,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/experimental-utils": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz",
|
||||
"integrity": "sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz",
|
||||
"integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==",
|
||||
"dependencies": {
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/typescript-estree": "4.28.4",
|
||||
"@typescript-eslint/scope-manager": "4.28.5",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/typescript-estree": "4.28.5",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
@ -2676,13 +2676,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.4.tgz",
|
||||
"integrity": "sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz",
|
||||
"integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/typescript-estree": "4.28.4",
|
||||
"@typescript-eslint/scope-manager": "4.28.5",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/typescript-estree": "4.28.5",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
@ -2702,12 +2702,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz",
|
||||
"integrity": "sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz",
|
||||
"integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/visitor-keys": "4.28.4"
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/visitor-keys": "4.28.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
|
||||
@ -2718,9 +2718,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.4.tgz",
|
||||
"integrity": "sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz",
|
||||
"integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA==",
|
||||
"engines": {
|
||||
"node": "^8.10.0 || ^10.13.0 || >=11.10.1"
|
||||
},
|
||||
@ -2730,12 +2730,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz",
|
||||
"integrity": "sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz",
|
||||
"integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/visitor-keys": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/visitor-keys": "4.28.5",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
@ -2775,11 +2775,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz",
|
||||
"integrity": "sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz",
|
||||
"integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
@ -3343,9 +3343,9 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"node_modules/chart.js": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz",
|
||||
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA=="
|
||||
},
|
||||
"node_modules/chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
@ -6790,9 +6790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.53.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.53.3.tgz",
|
||||
"integrity": "sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA==",
|
||||
"version": "2.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.54.0.tgz",
|
||||
"integrity": "sha512-RHzvstAVwm9A751NxWIbGPFXs3zL4qe/eYg+N7WwGtIXVLy1cK64MiU37+hXeFm1jqipK6DGgMi6Z2hhPuCC3A==",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
@ -9972,12 +9972,12 @@
|
||||
"integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA=="
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.4.tgz",
|
||||
"integrity": "sha512-s1oY4RmYDlWMlcV0kKPBaADn46JirZzvvH7c2CtAqxCY96S538JRBAzt83RrfkDheV/+G/vWNK0zek+8TB3Gmw==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.28.5.tgz",
|
||||
"integrity": "sha512-m31cPEnbuCqXtEZQJOXAHsHvtoDi9OVaeL5wZnO2KZTnkvELk+u6J6jHg+NzvWQxk+87Zjbc4lJS4NHmgImz6Q==",
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "4.28.4",
|
||||
"@typescript-eslint/scope-manager": "4.28.4",
|
||||
"@typescript-eslint/experimental-utils": "4.28.5",
|
||||
"@typescript-eslint/scope-manager": "4.28.5",
|
||||
"debug": "^4.3.1",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^3.1.0",
|
||||
@ -9986,14 +9986,14 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.4.tgz",
|
||||
"integrity": "sha512-OglKWOQRWTCoqMSy6pm/kpinEIgdcXYceIcH3EKWUl4S8xhFtN34GQRaAvTIZB9DD94rW7d/U7tUg3SYeDFNHA==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.28.5.tgz",
|
||||
"integrity": "sha512-bGPLCOJAa+j49hsynTaAtQIWg6uZd8VLiPcyDe4QPULsvQwLHGLSGKKcBN8/lBxIX14F74UEMK2zNDI8r0okwA==",
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.7",
|
||||
"@typescript-eslint/scope-manager": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/typescript-estree": "4.28.4",
|
||||
"@typescript-eslint/scope-manager": "4.28.5",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/typescript-estree": "4.28.5",
|
||||
"eslint-scope": "^5.1.1",
|
||||
"eslint-utils": "^3.0.0"
|
||||
},
|
||||
@ -10009,37 +10009,37 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.4.tgz",
|
||||
"integrity": "sha512-4i0jq3C6n+og7/uCHiE6q5ssw87zVdpUj1k6VlVYMonE3ILdFApEzTWgppSRG4kVNB/5jxnH+gTeKLMNfUelQA==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.28.5.tgz",
|
||||
"integrity": "sha512-NPCOGhTnkXGMqTznqgVbA5LqVsnw+i3+XA1UKLnAb+MG1Y1rP4ZSK9GX0kJBmAZTMIktf+dTwXToT6kFwyimbw==",
|
||||
"requires": {
|
||||
"@typescript-eslint/scope-manager": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/typescript-estree": "4.28.4",
|
||||
"@typescript-eslint/scope-manager": "4.28.5",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/typescript-estree": "4.28.5",
|
||||
"debug": "^4.3.1"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/scope-manager": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.4.tgz",
|
||||
"integrity": "sha512-ZJBNs4usViOmlyFMt9X9l+X0WAFcDH7EdSArGqpldXu7aeZxDAuAzHiMAeI+JpSefY2INHrXeqnha39FVqXb8w==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.28.5.tgz",
|
||||
"integrity": "sha512-PHLq6n9nTMrLYcVcIZ7v0VY1X7dK309NM8ya9oL/yG8syFINIMHxyr2GzGoBYUdv3NUfCOqtuqps0ZmcgnZTfQ==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/visitor-keys": "4.28.4"
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/visitor-keys": "4.28.5"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/types": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.4.tgz",
|
||||
"integrity": "sha512-3eap4QWxGqkYuEmVebUGULMskR6Cuoc/Wii0oSOddleP4EGx1tjLnZQ0ZP33YRoMDCs5O3j56RBV4g14T4jvww=="
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.28.5.tgz",
|
||||
"integrity": "sha512-MruOu4ZaDOLOhw4f/6iudyks/obuvvZUAHBDSW80Trnc5+ovmViLT2ZMDXhUV66ozcl6z0LJfKs1Usldgi/WCA=="
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.4.tgz",
|
||||
"integrity": "sha512-z7d8HK8XvCRyN2SNp+OXC2iZaF+O2BTquGhEYLKLx5k6p0r05ureUtgEfo5f6anLkhCxdHtCf6rPM1p4efHYDQ==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.5.tgz",
|
||||
"integrity": "sha512-FzJUKsBX8poCCdve7iV7ShirP8V+ys2t1fvamVeD1rWpiAnIm550a+BX/fmTHrjEpQJ7ZAn+Z7ZZwJjytk9rZw==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/visitor-keys": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"@typescript-eslint/visitor-keys": "4.28.5",
|
||||
"debug": "^4.3.1",
|
||||
"globby": "^11.0.3",
|
||||
"is-glob": "^4.0.1",
|
||||
@ -10063,11 +10063,11 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/visitor-keys": {
|
||||
"version": "4.28.4",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.4.tgz",
|
||||
"integrity": "sha512-NIAXAdbz1XdOuzqkJHjNKXKj8QQ4cv5cxR/g0uQhCYf/6//XrmfpaYsM7PnBcNbfvTDLUkqQ5TPNm1sozDdTWg==",
|
||||
"version": "4.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.5.tgz",
|
||||
"integrity": "sha512-dva/7Rr+EkxNWdJWau26xU/0slnFlkh88v3TsyTgRS/IIYFi5iIfpCFM4ikw0vQTFUR9FYSSyqgK4w64gsgxhg==",
|
||||
"requires": {
|
||||
"@typescript-eslint/types": "4.28.4",
|
||||
"@typescript-eslint/types": "4.28.5",
|
||||
"eslint-visitor-keys": "^2.0.0"
|
||||
}
|
||||
},
|
||||
@ -10508,9 +10508,9 @@
|
||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||
},
|
||||
"chart.js": {
|
||||
"version": "3.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.4.1.tgz",
|
||||
"integrity": "sha512-0R4mL7WiBcYoazIhrzSYnWcOw6RmrRn7Q4nKZNsBQZCBrlkZKodQbfeojCCo8eETPRCs1ZNTsAcZhIfyhyP61g=="
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.5.0.tgz",
|
||||
"integrity": "sha512-J1a4EAb1Gi/KbhwDRmoovHTRuqT8qdF0kZ4XgwxpGethJHUdDrkqyPYwke0a+BuvSeUxPf8Cos6AX2AB8H8GLA=="
|
||||
},
|
||||
"chartjs-adapter-moment": {
|
||||
"version": "1.0.0",
|
||||
@ -13241,9 +13241,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.53.3",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.53.3.tgz",
|
||||
"integrity": "sha512-79QIGP5DXz5ZHYnCPi3tLz+elOQi6gudp9YINdaJdjG0Yddubo6JRFUM//qCZ0Bap/GJrsUoEBVdSOc4AkMlRA==",
|
||||
"version": "2.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.54.0.tgz",
|
||||
"integrity": "sha512-RHzvstAVwm9A751NxWIbGPFXs3zL4qe/eYg+N7WwGtIXVLy1cK64MiU37+hXeFm1jqipK6DGgMi6Z2hhPuCC3A==",
|
||||
"requires": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
|
@ -58,13 +58,13 @@
|
||||
"@types/chart.js": "^2.9.34",
|
||||
"@types/codemirror": "5.60.2",
|
||||
"@types/grecaptcha": "^3.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.4",
|
||||
"@typescript-eslint/parser": "^4.28.4",
|
||||
"@typescript-eslint/eslint-plugin": "^4.28.5",
|
||||
"@typescript-eslint/parser": "^4.28.5",
|
||||
"@webcomponents/webcomponentsjs": "^2.5.0",
|
||||
"authentik-api": "file:api",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"base64-js": "^1.5.1",
|
||||
"chart.js": "^3.4.1",
|
||||
"chart.js": "^3.5.0",
|
||||
"chartjs-adapter-moment": "^1.0.0",
|
||||
"codemirror": "^5.62.2",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
@ -77,7 +77,7 @@
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.53.3",
|
||||
"rollup": "^2.54.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
|
@ -33,15 +33,7 @@ export function configureSentry(canDoPpi: boolean = false): Promise<Config> {
|
||||
}
|
||||
}
|
||||
if (hint.originalException instanceof Response) {
|
||||
const response = hint.originalException as Response;
|
||||
// We only care about server errors
|
||||
if (response.status < 500) {
|
||||
return null;
|
||||
}
|
||||
// Need to clone the response, otherwise the .text() and .json() can't be re-used
|
||||
const resCopy = response.clone();
|
||||
const body = await resCopy.json();
|
||||
event.message = `${response.status} ${response.url}: ${JSON.stringify(body)}`
|
||||
return null;
|
||||
}
|
||||
if (event.exception) {
|
||||
me().then(user => {
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2021.7.1-rc2";
|
||||
export const VERSION = "2021.7.3";
|
||||
export const PAGE_SIZE = 20;
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
@ -3,18 +3,20 @@ import { EVENT_REFRESH } from "../../constants";
|
||||
import { Form } from "./Form";
|
||||
|
||||
export abstract class ModelForm<T, PKT extends string | number> extends Form<T> {
|
||||
viewportCheck = true;
|
||||
|
||||
abstract loadInstance(pk: PKT): Promise<T>;
|
||||
|
||||
@property({attribute: false})
|
||||
set instancePk(value: PKT) {
|
||||
this._instancePk = value;
|
||||
if (this.isInViewport) {
|
||||
this.loadInstance(value).then(instance => {
|
||||
this.instance = instance;
|
||||
this.requestUpdate();
|
||||
});
|
||||
if (this.viewportCheck && !this.isInViewport) {
|
||||
return;
|
||||
}
|
||||
this.loadInstance(value).then((instance) => {
|
||||
this.instance = instance;
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
private _instancePk?: PKT;
|
||||
|
@ -1077,7 +1077,7 @@ msgstr "Delete Refresh Code"
|
||||
msgid "Delete Session"
|
||||
msgstr "Delete Session"
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
msgid "Delete account"
|
||||
msgstr "Delete account"
|
||||
|
||||
@ -1230,6 +1230,10 @@ msgstr "Duo activation"
|
||||
msgid "Duo push-notifications"
|
||||
msgstr "Duo push-notifications"
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Duration after which events will be deleted from the database."
|
||||
msgstr "Duration after which events will be deleted from the database."
|
||||
|
||||
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts
|
||||
msgid "Each provider has a different issuer, based on the application slug."
|
||||
msgstr "Each provider has a different issuer, based on the application slug."
|
||||
@ -1293,7 +1297,7 @@ msgstr "Either no applications are defined, or you don't have access to any."
|
||||
#: src/flows/stages/identification/IdentificationStage.ts
|
||||
#: src/pages/events/TransportForm.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
msgid "Email"
|
||||
@ -1408,6 +1412,10 @@ msgstr "Event Log"
|
||||
msgid "Event info"
|
||||
msgstr "Event info"
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Event retention"
|
||||
msgstr "Event retention"
|
||||
|
||||
#: src/pages/events/EventInfoPage.ts
|
||||
msgid "Event {0}"
|
||||
msgstr "Event {0}"
|
||||
@ -1416,13 +1424,16 @@ msgstr "Event {0}"
|
||||
msgid "Events"
|
||||
msgstr "Events"
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
msgid "Everything is ok."
|
||||
msgstr "Everything is ok."
|
||||
|
||||
#: src/pages/events/EventInfo.ts
|
||||
#: src/pages/events/EventInfo.ts
|
||||
#: src/pages/events/EventInfo.ts
|
||||
msgid "Exception"
|
||||
msgstr "Exception"
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
#: src/pages/flows/FlowViewPage.ts
|
||||
msgid "Execute"
|
||||
msgstr "Execute"
|
||||
@ -1475,7 +1486,6 @@ msgstr "Expiry date"
|
||||
msgid "Explicit Consent"
|
||||
msgstr "Explicit Consent"
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
#: src/pages/flows/FlowViewPage.ts
|
||||
msgid "Export"
|
||||
msgstr "Export"
|
||||
@ -1666,6 +1676,10 @@ msgstr "Forgot username or password?"
|
||||
msgid "Form didn't return a promise for submitting"
|
||||
msgstr "Form didn't return a promise for submitting"
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Format: \"weeks=3;days=2;hours=3,seconds=2\"."
|
||||
msgstr "Format: \"weeks=3;days=2;hours=3,seconds=2\"."
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Forward auth (domain level)"
|
||||
msgstr "Forward auth (domain level)"
|
||||
@ -1761,6 +1775,10 @@ msgstr "HTTP-Basic Password Key"
|
||||
msgid "HTTP-Basic Username Key"
|
||||
msgstr "HTTP-Basic Username Key"
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
msgid "HTTPS is not detected correctly"
|
||||
msgstr "HTTPS is not detected correctly"
|
||||
|
||||
#: src/pages/outposts/OutpostListPage.ts
|
||||
msgid "Health and Version"
|
||||
msgstr "Health and Version"
|
||||
@ -2060,6 +2078,10 @@ msgstr "Link to a user with identical email address. Can have security implicati
|
||||
msgid "Link to a user with identical username address. Can have security implications when a username is used with another source."
|
||||
msgstr "Link to a user with identical username address. Can have security implications when a username is used with another source."
|
||||
|
||||
#: src/pages/stages/invitation/InvitationListLink.ts
|
||||
msgid "Link to use the invitation."
|
||||
msgstr "Link to use the invitation."
|
||||
|
||||
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||
msgid "Link users on unique identifier"
|
||||
@ -2089,7 +2111,7 @@ msgstr "Load servers"
|
||||
#: src/flows/stages/prompt/PromptStage.ts
|
||||
#: src/pages/applications/ApplicationViewPage.ts
|
||||
#: src/pages/applications/ApplicationViewPage.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/utils.ts
|
||||
msgid "Loading"
|
||||
msgstr "Loading"
|
||||
@ -2148,6 +2170,7 @@ msgstr "Loading"
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/stages/invitation/InvitationListLink.ts
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
#: src/pages/stages/prompt/PromptStageForm.ts
|
||||
#: src/pages/stages/prompt/PromptStageForm.ts
|
||||
@ -2377,7 +2400,7 @@ msgstr "My Applications"
|
||||
#: src/pages/stages/user_login/UserLoginStageForm.ts
|
||||
#: src/pages/stages/user_logout/UserLogoutStageForm.ts
|
||||
#: src/pages/stages/user_write/UserWriteStageForm.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
#: src/pages/users/UserListPage.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
@ -2643,6 +2666,10 @@ msgstr "Optionally set this to your parent domain, if you want authentication an
|
||||
msgid "Order"
|
||||
msgstr "Order"
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Other global settings"
|
||||
msgstr "Other global settings"
|
||||
|
||||
#: src/pages/admin-overview/charts/OutpostStatusChart.ts
|
||||
msgid "Outdated outposts"
|
||||
msgstr "Outdated outposts"
|
||||
@ -2986,6 +3013,10 @@ msgstr "RSA-SHA384"
|
||||
msgid "RSA-SHA512"
|
||||
msgstr "RSA-SHA512"
|
||||
|
||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||
msgid "Re-authenticate with plex"
|
||||
msgstr "Re-authenticate with plex"
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Re-evaluate policies"
|
||||
msgstr "Re-evaluate policies"
|
||||
@ -3085,7 +3116,7 @@ msgstr "Request token URL"
|
||||
msgid "Required"
|
||||
msgstr "Required"
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||
msgstr "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||
@ -3239,6 +3270,10 @@ msgstr "Select a provider that this application should use. Alternatively, creat
|
||||
msgid "Select all rows"
|
||||
msgstr "Select all rows"
|
||||
|
||||
#: src/pages/stages/invitation/InvitationListLink.ts
|
||||
msgid "Select an enrollment flow"
|
||||
msgstr "Select an enrollment flow"
|
||||
|
||||
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
|
||||
msgid "Select an identification method."
|
||||
msgstr "Select an identification method."
|
||||
@ -3304,6 +3339,10 @@ msgstr "Separator: Static Separator Line"
|
||||
msgid "Server URI"
|
||||
msgstr "Server URI"
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
msgid "Server and client are further than 5 seconds apart."
|
||||
msgstr "Server and client are further than 5 seconds apart."
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Server name for which this provider's certificate is valid for."
|
||||
msgstr "Server name for which this provider's certificate is valid for."
|
||||
@ -3724,7 +3763,7 @@ msgstr "Successfully updated binding."
|
||||
msgid "Successfully updated certificate-key pair."
|
||||
msgstr "Successfully updated certificate-key pair."
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
msgid "Successfully updated details."
|
||||
msgstr "Successfully updated details."
|
||||
|
||||
@ -3881,6 +3920,10 @@ msgstr "System Overview"
|
||||
msgid "System Tasks"
|
||||
msgstr "System Tasks"
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "System status"
|
||||
msgstr "System status"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "System task exception"
|
||||
msgstr "System task exception"
|
||||
@ -4025,6 +4068,10 @@ msgstr "These policies control which users can access this application."
|
||||
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
|
||||
msgstr "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "This setting only affects new Events, as the expiration is saved per-event."
|
||||
msgstr "This setting only affects new Events, as the expiration is saved per-event."
|
||||
|
||||
#: src/pages/stages/invitation/InvitationStageForm.ts
|
||||
msgid "This stage can be included in enrollment flows to accept invitations."
|
||||
msgstr "This stage can be included in enrollment flows to accept invitations."
|
||||
@ -4248,7 +4295,7 @@ msgstr "Up-to-date!"
|
||||
#: src/pages/stages/StageListPage.ts
|
||||
#: src/pages/stages/prompt/PromptListPage.ts
|
||||
#: src/pages/tenants/TenantListPage.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
|
||||
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
|
||||
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
|
||||
@ -4351,7 +4398,7 @@ msgstr "Update User"
|
||||
msgid "Update available"
|
||||
msgstr "Update available"
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "Update details"
|
||||
msgstr "Update details"
|
||||
|
||||
@ -4484,7 +4531,7 @@ msgstr "User {0}"
|
||||
msgid "User's avatar"
|
||||
msgstr "User's avatar"
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
msgid "User's display name."
|
||||
msgstr "User's display name."
|
||||
@ -4504,7 +4551,7 @@ msgstr "Userinfo URL"
|
||||
#: src/flows/stages/identification/IdentificationStage.ts
|
||||
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
msgid "Username"
|
||||
@ -4592,6 +4639,8 @@ msgstr "Wait (max)"
|
||||
msgid "Wait (min)"
|
||||
msgstr "Wait (min)"
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
#: src/pages/events/RuleForm.ts
|
||||
#: src/pages/system-tasks/SystemTaskListPage.ts
|
||||
msgid "Warning"
|
||||
@ -4659,6 +4708,10 @@ msgstr "When selected, incoming assertion's Signatures will be validated against
|
||||
msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
|
||||
msgstr "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "When using an external logging solution for archiving, this can be set to \"minutes=5\"."
|
||||
msgstr "When using an external logging solution for archiving, this can be set to \"minutes=5\"."
|
||||
|
||||
#: src/flows/FlowExecutor.ts
|
||||
msgid "Whoops!"
|
||||
msgstr "Whoops!"
|
||||
|
@ -1071,7 +1071,7 @@ msgstr ""
|
||||
msgid "Delete Session"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
msgid "Delete account"
|
||||
msgstr ""
|
||||
|
||||
@ -1222,6 +1222,10 @@ msgstr ""
|
||||
msgid "Duo push-notifications"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Duration after which events will be deleted from the database."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/oauth2/OAuth2ProviderForm.ts
|
||||
msgid "Each provider has a different issuer, based on the application slug."
|
||||
msgstr ""
|
||||
@ -1285,7 +1289,7 @@ msgstr ""
|
||||
#: src/flows/stages/identification/IdentificationStage.ts
|
||||
#: src/pages/events/TransportForm.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
msgid "Email"
|
||||
@ -1400,6 +1404,10 @@ msgstr ""
|
||||
msgid "Event info"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Event retention"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/EventInfoPage.ts
|
||||
msgid "Event {0}"
|
||||
msgstr ""
|
||||
@ -1408,13 +1416,16 @@ msgstr ""
|
||||
msgid "Events"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
msgid "Everything is ok."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/EventInfo.ts
|
||||
#: src/pages/events/EventInfo.ts
|
||||
#: src/pages/events/EventInfo.ts
|
||||
msgid "Exception"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
#: src/pages/flows/FlowViewPage.ts
|
||||
msgid "Execute"
|
||||
msgstr ""
|
||||
@ -1467,7 +1478,6 @@ msgstr ""
|
||||
msgid "Explicit Consent"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
#: src/pages/flows/FlowViewPage.ts
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
@ -1658,6 +1668,10 @@ msgstr ""
|
||||
msgid "Form didn't return a promise for submitting"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Format: \"weeks=3;days=2;hours=3,seconds=2\"."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/proxy/ProxyProviderForm.ts
|
||||
msgid "Forward auth (domain level)"
|
||||
msgstr ""
|
||||
@ -1753,6 +1767,10 @@ msgstr ""
|
||||
msgid "HTTP-Basic Username Key"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
msgid "HTTPS is not detected correctly"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/OutpostListPage.ts
|
||||
msgid "Health and Version"
|
||||
msgstr ""
|
||||
@ -2052,6 +2070,10 @@ msgstr ""
|
||||
msgid "Link to a user with identical username address. Can have security implications when a username is used with another source."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/invitation/InvitationListLink.ts
|
||||
msgid "Link to use the invitation."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/sources/oauth/OAuthSourceForm.ts
|
||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||
msgid "Link users on unique identifier"
|
||||
@ -2081,7 +2103,7 @@ msgstr ""
|
||||
#: src/flows/stages/prompt/PromptStage.ts
|
||||
#: src/pages/applications/ApplicationViewPage.ts
|
||||
#: src/pages/applications/ApplicationViewPage.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/utils.ts
|
||||
msgid "Loading"
|
||||
msgstr ""
|
||||
@ -2140,6 +2162,7 @@ msgstr ""
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/stages/invitation/InvitationListLink.ts
|
||||
#: src/pages/stages/password/PasswordStageForm.ts
|
||||
#: src/pages/stages/prompt/PromptStageForm.ts
|
||||
#: src/pages/stages/prompt/PromptStageForm.ts
|
||||
@ -2369,7 +2392,7 @@ msgstr ""
|
||||
#: src/pages/stages/user_login/UserLoginStageForm.ts
|
||||
#: src/pages/stages/user_logout/UserLogoutStageForm.ts
|
||||
#: src/pages/stages/user_write/UserWriteStageForm.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
#: src/pages/users/UserListPage.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
@ -2635,6 +2658,10 @@ msgstr ""
|
||||
msgid "Order"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "Other global settings"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/charts/OutpostStatusChart.ts
|
||||
msgid "Outdated outposts"
|
||||
msgstr ""
|
||||
@ -2978,6 +3005,10 @@ msgstr ""
|
||||
msgid "RSA-SHA512"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/sources/plex/PlexSourceForm.ts
|
||||
msgid "Re-authenticate with plex"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/StageBindingForm.ts
|
||||
msgid "Re-evaluate policies"
|
||||
msgstr ""
|
||||
@ -3077,7 +3108,7 @@ msgstr ""
|
||||
msgid "Required"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
msgid "Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only."
|
||||
msgstr ""
|
||||
@ -3231,6 +3262,10 @@ msgstr ""
|
||||
msgid "Select all rows"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/invitation/InvitationListLink.ts
|
||||
msgid "Select an enrollment flow"
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/stages/authenticator_validate/AuthenticatorValidateStage.ts
|
||||
msgid "Select an identification method."
|
||||
msgstr ""
|
||||
@ -3296,6 +3331,10 @@ msgstr ""
|
||||
msgid "Server URI"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
msgid "Server and client are further than 5 seconds apart."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/ldap/LDAPProviderForm.ts
|
||||
msgid "Server name for which this provider's certificate is valid for."
|
||||
msgstr ""
|
||||
@ -3716,7 +3755,7 @@ msgstr ""
|
||||
msgid "Successfully updated certificate-key pair."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
msgid "Successfully updated details."
|
||||
msgstr ""
|
||||
|
||||
@ -3873,6 +3912,10 @@ msgstr ""
|
||||
msgid "System Tasks"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "System status"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "System task exception"
|
||||
msgstr ""
|
||||
@ -4010,6 +4053,10 @@ msgstr ""
|
||||
msgid "This provider will behave like a transparent reverse-proxy, except requests must be authenticated. If your upstream application uses HTTPS, make sure to connect to the outpost using HTTPS as well."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "This setting only affects new Events, as the expiration is saved per-event."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/invitation/InvitationStageForm.ts
|
||||
msgid "This stage can be included in enrollment flows to accept invitations."
|
||||
msgstr ""
|
||||
@ -4233,7 +4280,7 @@ msgstr ""
|
||||
#: src/pages/stages/StageListPage.ts
|
||||
#: src/pages/stages/prompt/PromptListPage.ts
|
||||
#: src/pages/tenants/TenantListPage.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
|
||||
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
|
||||
#: src/pages/user-settings/settings/UserSettingsAuthenticatorWebAuthn.ts
|
||||
@ -4336,7 +4383,7 @@ msgstr ""
|
||||
msgid "Update available"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSettingsPage.ts
|
||||
msgid "Update details"
|
||||
msgstr ""
|
||||
|
||||
@ -4469,7 +4516,7 @@ msgstr ""
|
||||
msgid "User's avatar"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
msgid "User's display name."
|
||||
msgstr ""
|
||||
@ -4489,7 +4536,7 @@ msgstr ""
|
||||
#: src/flows/stages/identification/IdentificationStage.ts
|
||||
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
#: src/pages/user-settings/UserSelfForm.ts
|
||||
#: src/pages/users/UserForm.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
msgid "Username"
|
||||
@ -4577,6 +4624,8 @@ msgstr ""
|
||||
msgid "Wait (min)"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
#: src/pages/admin-overview/cards/SystemStatusCard.ts
|
||||
#: src/pages/events/RuleForm.ts
|
||||
#: src/pages/system-tasks/SystemTaskListPage.ts
|
||||
msgid "Warning"
|
||||
@ -4644,6 +4693,10 @@ msgstr ""
|
||||
msgid "When this option is enabled, all executions of this policy will be logged. By default, only execution errors are logged."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/tenants/TenantForm.ts
|
||||
msgid "When using an external logging solution for archiving, this can be set to \"minutes=5\"."
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/FlowExecutor.ts
|
||||
msgid "Whoops!"
|
||||
msgstr ""
|
||||
|
@ -9,6 +9,7 @@ import "./cards/AdminStatusCard";
|
||||
import "./cards/BackupStatusCard";
|
||||
import "./cards/VersionStatusCard";
|
||||
import "./cards/WorkerStatusCard";
|
||||
import "./cards/SystemStatusCard";
|
||||
|
||||
import "./charts/FlowStatusChart";
|
||||
import "./charts/LDAPSyncStatusChart";
|
||||
@ -92,14 +93,18 @@ export class AdminOverviewPage extends LitElement {
|
||||
<ak-admin-status-version icon="pf-icon pf-icon-bundle" header=${t`Version`} headerLink="https://github.com/goauthentik/authentik/releases">
|
||||
</ak-admin-status-version>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container">
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-2-col-on-md pf-m-2-col-on-xl card-container">
|
||||
<ak-admin-status-card-backup icon="fa fa-database" header=${t`Backup status`} headerLink="#/administration/system-tasks">
|
||||
</ak-admin-status-card-backup>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-4-col-on-md pf-m-4-col-on-xl card-container">
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container">
|
||||
<ak-admin-status-card-workers icon="pf-icon pf-icon-server" header=${t`Workers`}>
|
||||
</ak-admin-status-card-workers>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-6-col pf-m-3-col-on-md pf-m-3-col-on-xl card-container">
|
||||
<ak-admin-status-system icon="pf-icon pf-icon-server" header=${t`System status`}>
|
||||
</ak-admin-status-system>
|
||||
</div>
|
||||
<div class="pf-l-grid__item pf-m-12-col row-divider">
|
||||
<hr>
|
||||
</div>
|
||||
|
46
web/src/pages/admin-overview/cards/SystemStatusCard.ts
Normal file
46
web/src/pages/admin-overview/cards/SystemStatusCard.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { customElement, html, TemplateResult } from "lit-element";
|
||||
import { AdminApi, System } from "authentik-api";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import { AdminStatusCard, AdminStatus } from "./AdminStatusCard";
|
||||
|
||||
@customElement("ak-admin-status-system")
|
||||
export class SystemStatusCard extends AdminStatusCard<System> {
|
||||
|
||||
now?: Date;
|
||||
|
||||
header = "OK";
|
||||
|
||||
getPrimaryValue(): Promise<System> {
|
||||
this.now = new Date();
|
||||
return new AdminApi(DEFAULT_CONFIG).adminSystemRetrieve();
|
||||
}
|
||||
|
||||
getStatus(value: System): Promise<AdminStatus> {
|
||||
if (!value.httpIsSecure && document.location.protocol === "https:") {
|
||||
this.header = t`Warning`;
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-exclamation-triangle pf-m-warning",
|
||||
message: t`HTTPS is not detected correctly`,
|
||||
});
|
||||
}
|
||||
const timeDiff = value.serverTime.getTime() - (this.now || new Date()).getTime();
|
||||
console.log(`authentik/: timediff ${timeDiff}`);
|
||||
if (timeDiff > 5000 || timeDiff < -5000) {
|
||||
this.header = t`Warning`;
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-exclamation-triangle pf-m-warning",
|
||||
message: t`Server and client are further than 5 seconds apart.`,
|
||||
});
|
||||
}
|
||||
return Promise.resolve<AdminStatus>({
|
||||
icon: "fa fa-check-circle pf-m-success",
|
||||
message: t`Everything is ok.`
|
||||
});
|
||||
}
|
||||
|
||||
renderValue(): TemplateResult {
|
||||
return html`${this.header}`;
|
||||
}
|
||||
|
||||
}
|
@ -8,7 +8,7 @@ import { until } from "lit-html/directives/until";
|
||||
import "../../elements/forms/HorizontalFormElement";
|
||||
|
||||
@customElement("ak-application-check-access-form")
|
||||
export class ApplicationCheckAccessForm extends Form<number> {
|
||||
export class ApplicationCheckAccessForm extends Form<{ forUser: number }> {
|
||||
|
||||
@property({attribute: false})
|
||||
application!: Application;
|
||||
@ -23,14 +23,19 @@ export class ApplicationCheckAccessForm extends Form<number> {
|
||||
return t`Successfully sent test-request.`;
|
||||
}
|
||||
|
||||
send = (data: number): Promise<PolicyTestResult> => {
|
||||
this.request = data;
|
||||
send = (data: { forUser: number }): Promise<PolicyTestResult> => {
|
||||
this.request = data.forUser;
|
||||
return new CoreApi(DEFAULT_CONFIG).coreApplicationsCheckAccessRetrieve({
|
||||
slug: this.application?.slug,
|
||||
forUser: data,
|
||||
forUser: data.forUser,
|
||||
}).then(result => this.result = result);
|
||||
};
|
||||
|
||||
resetForm(): void {
|
||||
super.resetForm();
|
||||
this.result = undefined;
|
||||
}
|
||||
|
||||
renderResult(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal
|
||||
|
@ -2,7 +2,7 @@ import { FlowsApi, ProvidersApi, LDAPProvider, CoreApi, FlowsInstancesListDesign
|
||||
import { t } from "@lingui/macro";
|
||||
import { customElement } from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import { DEFAULT_CONFIG, tenant } from "../../../api/Config";
|
||||
import { ModelForm } from "../../../elements/forms/ModelForm";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
@ -53,12 +53,18 @@ export class LDAPProviderFormPage extends ModelForm<LDAPProvider, number> {
|
||||
?required=${true}
|
||||
name="authorizationFlow">
|
||||
<select class="pf-c-form-control">
|
||||
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: "pk",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
}).then(flows => {
|
||||
return flows.results.map(flow => {
|
||||
return html`<option value=${ifDefined(flow.pk)} ?selected=${this.instance?.authorizationFlow === flow.pk}>${flow.name} (${flow.slug})</option>`;
|
||||
${until(tenant().then(t => {
|
||||
return new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: "pk",
|
||||
designation: FlowsInstancesListDesignationEnum.Authentication,
|
||||
}).then(flows => {
|
||||
return flows.results.map(flow => {
|
||||
let selected = flow.pk === t.flowAuthentication;
|
||||
if (this.instance?.authorizationFlow === flow.pk) {
|
||||
selected = true;
|
||||
}
|
||||
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
|
||||
});
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
|
@ -124,16 +124,16 @@ export class OAuthSourceForm extends ModelForm<OAuthSource, string> {
|
||||
<option value=${UserMatchingModeEnum.Identifier} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.Identifier}>
|
||||
${t`Link users on unique identifier`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
|
||||
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
|
||||
${t`Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
|
||||
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
|
||||
${t`Use the user's email address, but deny enrollment when the email address already exists.`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
|
||||
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
|
||||
${t`Link to a user with identical username address. Can have security implications when a username is used with another source.`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
|
||||
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
|
||||
${t`Use the user's username, but deny enrollment when the username already exists.`}
|
||||
</option>
|
||||
</select>
|
||||
|
@ -85,7 +85,13 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
||||
${t`Load servers`}
|
||||
</button>`;
|
||||
}
|
||||
return html`<ak-form-element-horizontal name="allowFriends">
|
||||
return html`
|
||||
<button class="pf-c-button pf-m-secondary" type="button" @click=${() => {
|
||||
this.doAuth();
|
||||
}}>
|
||||
${t`Re-authenticate with plex`}
|
||||
</button>
|
||||
<ak-form-element-horizontal name="allowFriends">
|
||||
<div class="pf-c-check">
|
||||
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.instance?.allowFriends, true)}>
|
||||
<label class="pf-c-check__label">
|
||||
@ -140,16 +146,16 @@ export class PlexSourceForm extends ModelForm<PlexSource, string> {
|
||||
<option value=${UserMatchingModeEnum.Identifier} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.Identifier}>
|
||||
${t`Link users on unique identifier`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
|
||||
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
|
||||
${t`Link to a user with identical email address. Can have security implications when a source doesn't validate email addresses`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
|
||||
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
|
||||
${t`Use the user's email address, but deny enrollment when the email address already exists.`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.EmailLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailLink}>
|
||||
<option value=${UserMatchingModeEnum.UsernameLink} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameLink}>
|
||||
${t`Link to a user with identical username address. Can have security implications when a username is used with another source.`}
|
||||
</option>
|
||||
<option value=${UserMatchingModeEnum.EmailDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.EmailDeny}>
|
||||
<option value=${UserMatchingModeEnum.UsernameDeny} ?selected=${this.instance?.userMatchingMode === UserMatchingModeEnum.UsernameDeny}>
|
||||
${t`Use the user's username, but deny enrollment when the username already exists.`}
|
||||
</option>
|
||||
</select>
|
||||
|
67
web/src/pages/stages/invitation/InvitationListLink.ts
Normal file
67
web/src/pages/stages/invitation/InvitationListLink.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import AKGlobal from "../../../authentik.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import { FlowsApi, FlowsInstancesListDesignationEnum } from "authentik-api";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
@customElement("ak-stage-invitation-list-link")
|
||||
export class InvitationListLink extends LitElement {
|
||||
|
||||
@property()
|
||||
invitation?: string
|
||||
|
||||
@property()
|
||||
selectedFlow?: string;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFForm, PFFormControl, PFFlex, PFDescriptionList, AKGlobal];
|
||||
}
|
||||
|
||||
renderLink(): string {
|
||||
return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?token=${this.invitation}`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<select class="pf-c-form-control" @change=${(ev: Event) => {
|
||||
const current = (ev.target as HTMLInputElement).value;
|
||||
this.selectedFlow = current;
|
||||
}}>
|
||||
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: "pk",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
}).then(flows => {
|
||||
return flows.results.map(flow => {
|
||||
return html`<option value=${flow.slug} ?selected=${flow.slug === this.selectedFlow}>${flow.slug}</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Link to use the invitation.`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<input class="pf-c-form-control" readonly type="text" value=${this.renderLink()} />
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>`;
|
||||
}
|
||||
|
||||
}
|
@ -8,6 +8,7 @@ import "../../../elements/buttons/SpinnerButton";
|
||||
import "../../../elements/forms/DeleteForm";
|
||||
import "../../../elements/forms/ModalForm";
|
||||
import "./InvitationForm";
|
||||
import "./InvitationListLink";
|
||||
import { TableColumn } from "../../../elements/table/Table";
|
||||
import { PAGE_SIZE } from "../../../constants";
|
||||
import { Invitation, StagesApi } from "authentik-api";
|
||||
@ -15,6 +16,8 @@ import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
|
||||
@customElement("ak-stage-invitation-list")
|
||||
export class InvitationListPage extends TablePage<Invitation> {
|
||||
expandable = true;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
@ -53,7 +56,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
return [
|
||||
html`${item.pk}`,
|
||||
html`${item.createdBy?.username}`,
|
||||
html`${item.expires?.toLocaleString()}`,
|
||||
html`${item.expires?.toLocaleString() || "-"}`,
|
||||
html`
|
||||
<ak-forms-delete
|
||||
.obj=${item}
|
||||
@ -75,6 +78,18 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: Invitation): TemplateResult {
|
||||
return html`
|
||||
<td role="cell" colspan="3">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-stage-invitation-list-link invitation=${item.pk}></ak-stage-invitation-list-link>
|
||||
</div>
|
||||
</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>`;
|
||||
}
|
||||
|
||||
renderToolbar(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
|
@ -162,6 +162,29 @@ export class TenantForm extends ModelForm<Tenant, string> {
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">
|
||||
${t`Other global settings`}
|
||||
</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Event retention`}
|
||||
?required=${true}
|
||||
name="eventRetention">
|
||||
<input type="text" value="${first(this.instance?.eventRetention, "days=365")}" class="pf-c-form-control" required>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Duration after which events will be deleted from the database.`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`When using an external logging solution for archiving, this can be set to "minutes=5".`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`This setting only affects new Events, as the expiration is saved per-event.`}
|
||||
</p>
|
||||
<p class="pf-c-form__helper-text">${t`Format: "weeks=3;days=2;hours=3,seconds=2".`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
|
@ -1,101 +0,0 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import AKGlobal from "../../authentik.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import { CoreApi, User } from "authentik-api";
|
||||
import { me } from "../../api/Users";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
|
||||
import "../../elements/forms/FormElement";
|
||||
import "../../elements/EmptyState";
|
||||
import "../../elements/forms/Form";
|
||||
import "../../elements/forms/HorizontalFormElement";
|
||||
import { until } from "lit-html/directives/until";
|
||||
|
||||
@customElement("ak-user-details")
|
||||
export class UserDetailsPage extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFCard, PFForm, PFFormControl, PFButton, AKGlobal];
|
||||
}
|
||||
|
||||
@property({attribute: false})
|
||||
user?: User;
|
||||
|
||||
firstUpdated(): void {
|
||||
me().then((user) => {
|
||||
this.user = user.user;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.user) {
|
||||
return html`<ak-empty-state
|
||||
?loading="${true}"
|
||||
header=${t`Loading`}>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
return html`<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">
|
||||
${t`Update details`}
|
||||
</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-form
|
||||
successMessage=${t`Successfully updated details.`}
|
||||
.send=${(data: unknown) => {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersUpdate({
|
||||
id: this.user?.pk || 0,
|
||||
userRequest: data as User
|
||||
});
|
||||
}}>
|
||||
<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Username`}
|
||||
?required=${true}
|
||||
name="username">
|
||||
<input type="text" value="${ifDefined(this.user?.username)}" class="pf-c-form-control" required>
|
||||
<p class="pf-c-form__helper-text">${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Name`}
|
||||
?required=${true}
|
||||
name="name">
|
||||
<input type="text" value="${ifDefined(this.user?.name)}" class="pf-c-form-control" required>
|
||||
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Email`}
|
||||
?required=${true}
|
||||
name="email">
|
||||
<input type="email" value="${ifDefined(this.user?.email)}" class="pf-c-form-control" required>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<button class="pf-c-button pf-m-primary">
|
||||
${t`Update`}
|
||||
</button>
|
||||
${until(tenant().then(tenant => {
|
||||
if (tenant.flowUnenrollment) {
|
||||
return html`<a class="pf-c-button pf-m-danger"
|
||||
href="/if/flow/${tenant.flowUnenrollment}">
|
||||
${t`Delete account`}
|
||||
</a>`;
|
||||
}
|
||||
return html``;
|
||||
}))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</ak-form>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
}
|
100
web/src/pages/user-settings/UserSelfForm.ts
Normal file
100
web/src/pages/user-settings/UserSelfForm.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { customElement, html, TemplateResult } from "lit-element";
|
||||
import { CoreApi, UserSelf } from "authentik-api";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { DEFAULT_CONFIG, tenant } from "../../api/Config";
|
||||
import "../../elements/forms/FormElement";
|
||||
import "../../elements/EmptyState";
|
||||
import "../../elements/forms/Form";
|
||||
import "../../elements/forms/HorizontalFormElement";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import { ModelForm } from "../../elements/forms/ModelForm";
|
||||
|
||||
@customElement("ak-user-self-form")
|
||||
export class UserSelfForm extends ModelForm<UserSelf, number> {
|
||||
viewportCheck = false;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
loadInstance(pk: number): Promise<UserSelf> {
|
||||
return new CoreApi(DEFAULT_CONFIG).coreUsersMeRetrieve().then((su) => {
|
||||
return su.user;
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return t`Successfully updated details.`;
|
||||
}
|
||||
|
||||
send = (data: UserSelf): Promise<UserSelf> => {
|
||||
return new CoreApi(DEFAULT_CONFIG)
|
||||
.coreUsersUpdateSelfUpdate({
|
||||
userSelfRequest: data,
|
||||
})
|
||||
.then((su) => {
|
||||
return su.user;
|
||||
});
|
||||
};
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
if (!this.instance) {
|
||||
return html`<ak-empty-state ?loading="${true}" header=${t`Loading`}> </ak-empty-state>`;
|
||||
}
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal label=${t`Username`} ?required=${true} name="username">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.username)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Name`} ?required=${true} name="name">
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${t`User's display name.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Email`} name="email">
|
||||
<input
|
||||
type="email"
|
||||
value="${ifDefined(this.instance?.email)}"
|
||||
class="pf-c-form-control"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<div class="pf-c-form__group pf-m-action">
|
||||
<div class="pf-c-form__horizontal-group">
|
||||
<div class="pf-c-form__actions">
|
||||
<button
|
||||
@click=${(ev: Event) => {
|
||||
return this.submit(ev);
|
||||
}}
|
||||
class="pf-c-button pf-m-primary"
|
||||
>
|
||||
${t`Update`}
|
||||
</button>
|
||||
${until(
|
||||
tenant().then((tenant) => {
|
||||
if (tenant.flowUnenrollment) {
|
||||
return html`<a
|
||||
class="pf-c-button pf-m-danger"
|
||||
href="/if/flow/${tenant.flowUnenrollment}"
|
||||
>
|
||||
${t`Delete account`}
|
||||
</a>`;
|
||||
}
|
||||
return html``;
|
||||
}),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>`;
|
||||
}
|
||||
}
|
@ -20,7 +20,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import "../../elements/Tabs";
|
||||
import "../../elements/PageHeader";
|
||||
import "./tokens/UserTokenList";
|
||||
import "./UserDetailsPage";
|
||||
import "./UserSelfForm";
|
||||
import "./settings/UserSettingsAuthenticatorDuo";
|
||||
import "./settings/UserSettingsAuthenticatorStatic";
|
||||
import "./settings/UserSettingsAuthenticatorTOTP";
|
||||
@ -95,8 +95,17 @@ export class UserSettingsPage extends LitElement {
|
||||
description=${t`Configure settings relevant to your user profile.`}>
|
||||
</ak-page-header>
|
||||
<ak-tabs ?vertical="${true}" style="height: 100%;">
|
||||
<section slot="page-details" data-tab-title="${t`User details`}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-user-details></ak-user-details>
|
||||
<section
|
||||
slot="page-details"
|
||||
data-tab-title="${t`User details`}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__title">${t`Update details`}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-user-self-form .instancePk=${1}></ak-user-self-form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section slot="page-tokens" data-tab-title="${t`Tokens`}" class="pf-c-page__main-section pf-m-no-padding-mobile">
|
||||
<ak-user-token-list></ak-user-token-list>
|
||||
|
@ -58,9 +58,8 @@ export class UserForm extends ModelForm<User, number> {
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Email`}
|
||||
?required=${true}
|
||||
name="email">
|
||||
<input type="email" autocomplete="off" value="${ifDefined(this.instance?.email)}" class="pf-c-form-control" required>
|
||||
<input type="email" autocomplete="off" value="${ifDefined(this.instance?.email)}" class="pf-c-form-control">
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
name="isActive">
|
||||
|
@ -8,6 +8,7 @@ To create a local development setup for authentik, you need the following:
|
||||
|
||||
- Python 3.9
|
||||
- pipenv, which is used to manage dependencies, and can be installed with `pip install pipenv`
|
||||
- Go 1.16
|
||||
- PostgreSQL (any recent version will do)
|
||||
- Redis (any recent version will do)
|
||||
|
||||
@ -24,11 +25,13 @@ log_level: debug
|
||||
secret_key: "A long key you can generate with `pwgen 40 1` for example"
|
||||
```
|
||||
|
||||
Afterwards, you can start authentik by running `./manage.py runserver`. Generally speaking, authentik is a Django application.
|
||||
Afterwards, you can start authentik by running `make run`.
|
||||
|
||||
Generally speaking, authentik is a Django application, ran by gunicorn, proxied by a Go application. The Go application serves static files.
|
||||
|
||||
Most functions and classes have type-hints and docstrings, so it is recommended to install a Python Type-checking Extension in your IDE to navigate around the code.
|
||||
|
||||
Before committing code, run `make lint` to ensure your code is formatted well. This also requires `pyright`, which can be installed with npm.
|
||||
Before committing code, run `make lint` to ensure your code is formatted well. This also requires `pyright@1.1.136`, which can be installed with npm.
|
||||
|
||||
Run `make gen` to generate an updated OpenAPI document for any changes you made.
|
||||
|
||||
|
@ -12,11 +12,11 @@ This installation method is for test-setups and small-scale productive setups.
|
||||
|
||||
## Preparation
|
||||
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.7.1-rc2/docker-compose.yml). Place it in a directory of your choice.
|
||||
Download the latest `docker-compose.yml` from [here](https://raw.githubusercontent.com/goauthentik/authentik/version/2021.7.3/docker-compose.yml). Place it in a directory of your choice.
|
||||
|
||||
To optionally enable error-reporting, run `echo AUTHENTIK_ERROR_REPORTING__ENABLED=true >> .env`
|
||||
|
||||
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.7.1-rc2 >> .env`
|
||||
To optionally deploy a different version run `echo AUTHENTIK_TAG=2021.7.3 >> .env`
|
||||
|
||||
If this is a fresh authentik install run the following commands to generate a password:
|
||||
|
||||
|
@ -11,7 +11,7 @@ version: "3.5"
|
||||
|
||||
services:
|
||||
authentik_proxy:
|
||||
image: ghcr.io/goauthentik/proxy:2021.7.1-rc2
|
||||
image: ghcr.io/goauthentik/proxy:2021.7.3
|
||||
ports:
|
||||
- 4180:4180
|
||||
- 4443:4443
|
||||
@ -21,7 +21,7 @@ services:
|
||||
AUTHENTIK_TOKEN: token-generated-by-authentik
|
||||
# Or, for the LDAP Outpost
|
||||
authentik_proxy:
|
||||
image: ghcr.io/goauthentik/ldap:2021.7.1-rc2
|
||||
image: ghcr.io/goauthentik/ldap:2021.7.3
|
||||
ports:
|
||||
- 389:3389
|
||||
environment:
|
||||
|
@ -14,7 +14,7 @@ metadata:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.7.1-rc2
|
||||
app.kubernetes.io/version: 2021.7.3
|
||||
name: authentik-outpost-api
|
||||
stringData:
|
||||
authentik_host: "__AUTHENTIK_URL__"
|
||||
@ -29,7 +29,7 @@ metadata:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.7.1-rc2
|
||||
app.kubernetes.io/version: 2021.7.3
|
||||
name: authentik-outpost
|
||||
spec:
|
||||
ports:
|
||||
@ -54,7 +54,7 @@ metadata:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.7.1-rc2
|
||||
app.kubernetes.io/version: 2021.7.3
|
||||
name: authentik-outpost
|
||||
spec:
|
||||
selector:
|
||||
@ -62,14 +62,14 @@ spec:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.7.1-rc2
|
||||
app.kubernetes.io/version: 2021.7.3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.7.1-rc2
|
||||
app.kubernetes.io/version: 2021.7.3
|
||||
spec:
|
||||
containers:
|
||||
- env:
|
||||
@ -88,7 +88,7 @@ spec:
|
||||
secretKeyRef:
|
||||
key: authentik_host_insecure
|
||||
name: authentik-outpost-api
|
||||
image: ghcr.io/goauthentik/proxy:2021.7.1-rc2
|
||||
image: ghcr.io/goauthentik/proxy:2021.7.3
|
||||
name: proxy
|
||||
ports:
|
||||
- containerPort: 4180
|
||||
@ -110,7 +110,7 @@ metadata:
|
||||
app.kubernetes.io/instance: __OUTPOST_NAME__
|
||||
app.kubernetes.io/managed-by: goauthentik.io
|
||||
app.kubernetes.io/name: authentik-proxy
|
||||
app.kubernetes.io/version: 2021.7.1-rc2
|
||||
app.kubernetes.io/version: 2021.7.3
|
||||
name: authentik-outpost
|
||||
spec:
|
||||
rules:
|
||||
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Reference in New Issue
Block a user