Compare commits
94 Commits
version/20
...
version/20
| Author | SHA1 | Date | |
|---|---|---|---|
| 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 | |||
| 3041a30193 | |||
| 1e28a1e311 | |||
| 5a1b912b76 | |||
| 464c27ef17 | |||
| a745022f06 | |||
| 0b34f70205 | |||
| a4b051fcc1 | |||
| 5ff3e9b418 | |||
| 8ae7403abc | |||
| f6e1bfdfc8 | |||
| aca3a5c458 | |||
| d16c24fd53 | |||
| 6a8be0dc71 | |||
| 81b9b37e5e | |||
| 22b01962fb | |||
| 86cc99be35 | |||
| 416f917c4a | |||
| f77bece790 | |||
| a8dd846437 | |||
| 4c50769040 | |||
| 34189fcc06 | |||
| fb5c8f3d7f | |||
| 049a55a761 | |||
| 4cd53f3d11 | |||
| 0d0dcf8de0 | |||
| 8cd1223081 | |||
| 1b4654bb1d | |||
| 0a3fade1fd | |||
| ff64814f40 | |||
| cbeb6e58ac | |||
| 285a9b8b1d | |||
| 66bfa6879d | |||
| c05240afbf | |||
| 7370dd5f3f | |||
| 477c8b099e | |||
| 2c761da883 | |||
| 75070232b1 | |||
| 690b35e1a3 | |||
| bd67f2362f | |||
| 896e5adce2 | |||
| 7f25b6311d | |||
| 253f345fc4 | |||
| a3abbcec6a | |||
| 70e000d327 | |||
| a7467e6740 | |||
| b3da94bbb8 | |||
| e62f5a75e4 |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2021.7.1-rc1
|
||||
current_version = 2021.7.2
|
||||
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.
|
||||
21
.github/workflows/release.yml
vendored
21
.github/workflows/release.yml
vendored
@ -33,14 +33,14 @@ jobs:
|
||||
with:
|
||||
push: ${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
beryju/authentik:2021.7.1-rc1,
|
||||
beryju/authentik:2021.7.2,
|
||||
beryju/authentik:latest,
|
||||
ghcr.io/goauthentik/server:2021.7.1-rc1,
|
||||
ghcr.io/goauthentik/server:2021.7.2,
|
||||
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-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.2', '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-rc1,
|
||||
beryju/authentik-proxy:2021.7.2,
|
||||
beryju/authentik-proxy:latest,
|
||||
ghcr.io/goauthentik/proxy:2021.7.1-rc1,
|
||||
ghcr.io/goauthentik/proxy:2021.7.2,
|
||||
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-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.2', '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-rc1,
|
||||
beryju/authentik-ldap:2021.7.2,
|
||||
beryju/authentik-ldap:latest,
|
||||
ghcr.io/goauthentik/ldap:2021.7.1-rc1,
|
||||
ghcr.io/goauthentik/ldap:2021.7.2,
|
||||
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-rc1', 'rc') }}
|
||||
if: ${{ github.event_name == 'release' && !contains('2021.7.2', 'rc') }}
|
||||
run: |
|
||||
docker pull beryju/authentik-ldap:latest
|
||||
docker tag beryju/authentik-ldap:latest beryju/authentik-ldap:stable
|
||||
@ -176,6 +176,7 @@ jobs:
|
||||
SENTRY_PROJECT: authentik
|
||||
SENTRY_URL: https://sentry.beryju.org
|
||||
with:
|
||||
version: authentik@2021.7.1-rc1
|
||||
version: authentik@2021.7.2
|
||||
environment: beryjuorg-prod
|
||||
sourcemaps: './web/dist'
|
||||
url_prefix: '~/static/dist'
|
||||
|
||||
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.
|
||||
66
Pipfile.lock
generated
66
Pipfile.lock
generated
@ -122,19 +122,19 @@
|
||||
},
|
||||
"boto3": {
|
||||
"hashes": [
|
||||
"sha256:13e60f88d13161df951d6e52bd483cdbe1a36a31f818746289d8ba0879465710",
|
||||
"sha256:3be2f259b279d69495433e3288db3670817fdb1813cfde92abf867bba3ad8148"
|
||||
"sha256:a012570d3535ec6c4db97e60ef51c2f39f38246429e1455cecc26c633ed81c10",
|
||||
"sha256:c7f45b0417395d3020c98cdc10f942939883018210e29dbfe6fbfc0a74e503ec"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.18.3"
|
||||
"version": "==1.18.7"
|
||||
},
|
||||
"botocore": {
|
||||
"hashes": [
|
||||
"sha256:0b6f378c9efbc72eee61aba1e16cab90bde53a37bd2d861f6435552fd7030adf",
|
||||
"sha256:285ab9459cdd49d4a9322692c6e13772b97af723a03c0eed519b589446491a5b"
|
||||
"sha256:34c8b151a25616ed7791218f6d7780c3a97725fe3ceeaa28085b345a8513af6e",
|
||||
"sha256:dcf399d21170bb899e00d2a693bddcc79e61471fbfead8500a65578700a3190a"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.21.3"
|
||||
"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": [
|
||||
@ -1436,11 +1436,11 @@
|
||||
},
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:6021561b2e87ed6b3c93c2682ac50079c65ab08f1e4e0277ba38f97e0e492185",
|
||||
"sha256:a670dd7af3fe603f51aa7117462588b7c3bdcd58007edfaee752bf82eceecd28"
|
||||
"sha256:7b963d1c590d490f60d2973e57437115978d3a2529843f160b5003b721e1e925",
|
||||
"sha256:83e494b02d75d07d4e347b27c066fd791c0c74fc96c613d1ea3de0c82c48168f"
|
||||
],
|
||||
"markers": "python_version ~= '3.6'",
|
||||
"version": "==2.6.4"
|
||||
"version": "==2.6.5"
|
||||
},
|
||||
"attrs": {
|
||||
"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": {
|
||||
@ -1684,11 +1684,11 @@
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:2a971129fb2d594068913a7e531d4b6d2785b2a68c6857e2baa40d3214da30f4",
|
||||
"sha256:a622c4c4c79dc8fe5e784efccacec3afe9d5e5ffab5fda2264fb5afa7c9b5797"
|
||||
"sha256:1f333dc72ef7f5ea166b3230936ebcfb1f3b722e76c980cb9fe6b9f95e8d3172",
|
||||
"sha256:748f81e5776d6273a6619506e08f1b48ff9bcb8198366a56821cf11aac14fc87"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.9.4"
|
||||
"version": "==2.9.5"
|
||||
},
|
||||
"pylint-django": {
|
||||
"hashes": [
|
||||
|
||||
@ -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-rc1"
|
||||
__version__ = "2021.7.2"
|
||||
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:
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
"""Groups API Viewset"""
|
||||
from django.db.models.query import QuerySet
|
||||
from django_filters.filters import ModelMultipleChoiceFilter
|
||||
from django_filters.filterset import FilterSet
|
||||
from rest_framework.fields import BooleanField, CharField, JSONField
|
||||
from rest_framework.serializers import ListSerializer, ModelSerializer
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
@ -57,13 +59,32 @@ class GroupSerializer(ModelSerializer):
|
||||
]
|
||||
|
||||
|
||||
class GroupFilter(FilterSet):
|
||||
"""Filter for groups"""
|
||||
|
||||
members_by_username = ModelMultipleChoiceFilter(
|
||||
field_name="users__username",
|
||||
to_field_name="username",
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
members_by_pk = ModelMultipleChoiceFilter(
|
||||
field_name="users",
|
||||
queryset=User.objects.all(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
model = Group
|
||||
fields = ["name", "is_superuser", "members_by_pk", "members_by_username"]
|
||||
|
||||
|
||||
class GroupViewSet(UsedByMixin, ModelViewSet):
|
||||
"""Group Viewset"""
|
||||
|
||||
queryset = Group.objects.all()
|
||||
serializer_class = GroupSerializer
|
||||
search_fields = ["name", "is_superuser"]
|
||||
filterset_fields = ["name", "is_superuser"]
|
||||
filterset_class = GroupFilter
|
||||
ordering = ["name"]
|
||||
|
||||
def _filter_queryset_for_list(self, queryset: QuerySet) -> QuerySet:
|
||||
|
||||
@ -128,7 +128,14 @@ class UsersFilter(FilterSet):
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ["username", "name", "is_active", "is_superuser", "attributes"]
|
||||
fields = [
|
||||
"username",
|
||||
"email",
|
||||
"name",
|
||||
"is_active",
|
||||
"is_superuser",
|
||||
"attributes",
|
||||
]
|
||||
|
||||
|
||||
class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
@ -136,7 +143,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
|
||||
queryset = User.objects.none()
|
||||
serializer_class = UserSerializer
|
||||
search_fields = ["username", "name", "is_active"]
|
||||
search_fields = ["username", "name", "is_active", "email"]
|
||||
filterset_class = UsersFilter
|
||||
|
||||
def get_queryset(self): # pragma: no cover
|
||||
@ -206,6 +213,6 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
return queryset
|
||||
|
||||
def filter_queryset(self, queryset):
|
||||
if self.request.user.has_perm("authentik_core.view_group"):
|
||||
if self.request.user.has_perm("authentik_core.view_user"):
|
||||
return self._filter_queryset_for_list(queryset)
|
||||
return super().filter_queryset(queryset)
|
||||
|
||||
@ -491,8 +491,8 @@ class PropertyMapping(SerializerModel, ManagedModel):
|
||||
evaluator.set_context(user, request, self, **kwargs)
|
||||
try:
|
||||
return evaluator.evaluate(self.expression)
|
||||
except (ValueError, SyntaxError) as exc:
|
||||
raise PropertyMappingExpressionException from exc
|
||||
except Exception as exc:
|
||||
raise PropertyMappingExpressionException(str(exc)) from exc
|
||||
|
||||
def __str__(self):
|
||||
return f"Property Mapping {self.name}"
|
||||
|
||||
@ -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]:
|
||||
|
||||
@ -31,7 +31,7 @@ class TestPropertyMappings(TestCase):
|
||||
"""Test expression error"""
|
||||
expr = "return aaa"
|
||||
mapping = PropertyMapping.objects.create(name="test", expression=expr)
|
||||
with self.assertRaises(NameError):
|
||||
with self.assertRaises(PropertyMappingExpressionException):
|
||||
mapping.evaluate(None, None)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
|
||||
@ -44,7 +44,7 @@ class TestPropertyMappings(TestCase):
|
||||
expr = "return aaa"
|
||||
request = self.factory.get("/")
|
||||
mapping = PropertyMapping.objects.create(name="test", expression=expr)
|
||||
with self.assertRaises(NameError):
|
||||
with self.assertRaises(PropertyMappingExpressionException):
|
||||
mapping.evaluate(get_anonymous_user(), request)
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.PROPERTY_MAPPING_EXCEPTION, context__expression=expr
|
||||
|
||||
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))
|
||||
|
||||
|
||||
67
authentik/lib/tests/test_http.py
Normal file
67
authentik/lib/tests/test_http.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Test HTTP Helpers"""
|
||||
from django.test import RequestFactory, TestCase
|
||||
|
||||
from authentik.core.models import (
|
||||
USER_ATTRIBUTE_CAN_OVERRIDE_IP,
|
||||
Token,
|
||||
TokenIntents,
|
||||
User,
|
||||
)
|
||||
from authentik.lib.utils.http import (
|
||||
OUTPOST_REMOTE_IP_HEADER,
|
||||
OUTPOST_TOKEN_HEADER,
|
||||
get_client_ip,
|
||||
)
|
||||
|
||||
|
||||
class TestHTTP(TestCase):
|
||||
"""Test HTTP Helpers"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_normal(self):
|
||||
"""Test normal request"""
|
||||
request = self.factory.get("/")
|
||||
self.assertEqual(get_client_ip(request), "127.0.0.1")
|
||||
|
||||
def test_forward_for(self):
|
||||
"""Test x-forwarded-for request"""
|
||||
request = self.factory.get("/", HTTP_X_FORWARDED_FOR="127.0.0.2")
|
||||
self.assertEqual(get_client_ip(request), "127.0.0.2")
|
||||
|
||||
def test_fake_outpost(self):
|
||||
"""Test faked IP which is overridden by an outpost"""
|
||||
token = Token.objects.create(
|
||||
identifier="test", user=self.user, intent=TokenIntents.INTENT_API
|
||||
)
|
||||
# Invalid, non-existant token
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
**{
|
||||
OUTPOST_REMOTE_IP_HEADER: "1.2.3.4",
|
||||
OUTPOST_TOKEN_HEADER: "abc",
|
||||
},
|
||||
)
|
||||
self.assertEqual(get_client_ip(request), "127.0.0.1")
|
||||
# Invalid, user doesn't have permisions
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
**{
|
||||
OUTPOST_REMOTE_IP_HEADER: "1.2.3.4",
|
||||
OUTPOST_TOKEN_HEADER: token.key,
|
||||
},
|
||||
)
|
||||
self.assertEqual(get_client_ip(request), "127.0.0.1")
|
||||
# Valid
|
||||
self.user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||
self.user.save()
|
||||
request = self.factory.get(
|
||||
"/",
|
||||
**{
|
||||
OUTPOST_REMOTE_IP_HEADER: "1.2.3.4",
|
||||
OUTPOST_TOKEN_HEADER: token.key,
|
||||
},
|
||||
)
|
||||
self.assertEqual(get_client_ip(request), "1.2.3.4")
|
||||
@ -40,24 +40,30 @@ def _get_outpost_override_ip(request: HttpRequest) -> Optional[str]:
|
||||
or OUTPOST_TOKEN_HEADER not in request.META
|
||||
):
|
||||
return None
|
||||
fake_ip = request.META[OUTPOST_REMOTE_IP_HEADER]
|
||||
tokens = Token.filter_not_expired(
|
||||
key=request.META.get(OUTPOST_TOKEN_HEADER), intent=TokenIntents.INTENT_API
|
||||
)
|
||||
if not tokens.exists():
|
||||
LOGGER.warning("Attempted remote-ip override without token")
|
||||
LOGGER.warning("Attempted remote-ip override without token", fake_ip=fake_ip)
|
||||
return None
|
||||
user = tokens.first().user
|
||||
if user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||
if not user.group_attributes().get(USER_ATTRIBUTE_CAN_OVERRIDE_IP, False):
|
||||
LOGGER.warning(
|
||||
"Remote-IP override: user doesn't have permission",
|
||||
user=user,
|
||||
fake_ip=fake_ip,
|
||||
)
|
||||
return None
|
||||
return request.META[OUTPOST_REMOTE_IP_HEADER]
|
||||
return fake_ip
|
||||
|
||||
|
||||
def get_client_ip(request: Optional[HttpRequest]) -> str:
|
||||
"""Attempt to get the client's IP by checking common HTTP Headers.
|
||||
Returns none if no IP Could be found"""
|
||||
if request:
|
||||
override = _get_outpost_override_ip(request)
|
||||
if override:
|
||||
return override
|
||||
return _get_client_ip_from_meta(request.META)
|
||||
return DEFAULT_IP
|
||||
if not request:
|
||||
return DEFAULT_IP
|
||||
override = _get_outpost_override_ip(request)
|
||||
if override:
|
||||
return override
|
||||
return _get_client_ip_from_meta(request.META)
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -344,12 +344,13 @@ class Outpost(ManagedModel):
|
||||
users = User.objects.filter(username=self.user_identifier)
|
||||
if not users.exists():
|
||||
user: User = User.objects.create(username=self.user_identifier)
|
||||
user.attributes[USER_ATTRIBUTE_SA] = True
|
||||
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||
user.set_unusable_password()
|
||||
user.save()
|
||||
else:
|
||||
user = users.first()
|
||||
user.attributes[USER_ATTRIBUTE_SA] = True
|
||||
user.attributes[USER_ATTRIBUTE_CAN_OVERRIDE_IP] = True
|
||||
user.save()
|
||||
# To ensure the user only has the correct permissions, we delete all of them and re-add
|
||||
# the ones the user needs
|
||||
with transaction.atomic():
|
||||
|
||||
@ -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), {})
|
||||
111
authentik/providers/oauth2/tests/test_userinfo.py
Normal file
111
authentik/providers/oauth2/tests/test_userinfo.py
Normal file
@ -0,0 +1,111 @@
|
||||
"""Test userinfo view"""
|
||||
import json
|
||||
from dataclasses import asdict
|
||||
|
||||
from django.urls import reverse
|
||||
from django.utils.encoding import force_str
|
||||
|
||||
from authentik.core.models import Application, User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.generators import (
|
||||
generate_client_id,
|
||||
generate_client_secret,
|
||||
)
|
||||
from authentik.providers.oauth2.models import (
|
||||
IDToken,
|
||||
OAuth2Provider,
|
||||
RefreshToken,
|
||||
ScopeMapping,
|
||||
)
|
||||
from authentik.providers.oauth2.tests.utils import OAuthTestCase
|
||||
|
||||
|
||||
class TestUserinfo(OAuthTestCase):
|
||||
"""Test token view"""
|
||||
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
ObjectManager().run()
|
||||
self.app = Application.objects.create(name="test", slug="test")
|
||||
self.provider: OAuth2Provider = OAuth2Provider.objects.create(
|
||||
name="test",
|
||||
client_id=generate_client_id(),
|
||||
client_secret=generate_client_secret(),
|
||||
authorization_flow=Flow.objects.first(),
|
||||
redirect_uris="",
|
||||
rsa_key=CertificateKeyPair.objects.first(),
|
||||
)
|
||||
self.provider.property_mappings.set(ScopeMapping.objects.all())
|
||||
# Needs to be assigned to an application for iss to be set
|
||||
self.app.provider = self.provider
|
||||
self.app.save()
|
||||
self.user = User.objects.get(username="akadmin")
|
||||
self.token: RefreshToken = RefreshToken.objects.create(
|
||||
provider=self.provider,
|
||||
user=self.user,
|
||||
access_token=generate_client_id(),
|
||||
refresh_token=generate_client_id(),
|
||||
_scope="openid user profile",
|
||||
_id_token=json.dumps(
|
||||
asdict(
|
||||
IDToken("foo", "bar"),
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
def test_userinfo_normal(self):
|
||||
"""test user info with all normal scopes"""
|
||||
res = self.client.get(
|
||||
reverse("authentik_providers_oauth2:userinfo"),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.access_token}",
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
force_str(res.content),
|
||||
{
|
||||
"name": "authentik Default Admin",
|
||||
"given_name": "authentik Default Admin",
|
||||
"family_name": "",
|
||||
"preferred_username": "akadmin",
|
||||
"nickname": "akadmin",
|
||||
"groups": ["authentik Admins"],
|
||||
"sub": "bar",
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
def test_userinfo_invalid_scope(self):
|
||||
"""test user info with a broken scope"""
|
||||
scope = ScopeMapping.objects.create(
|
||||
name="test", scope_name="openid", expression="q"
|
||||
)
|
||||
self.provider.property_mappings.add(scope)
|
||||
|
||||
res = self.client.get(
|
||||
reverse("authentik_providers_oauth2:userinfo"),
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.token.access_token}",
|
||||
)
|
||||
self.assertJSONEqual(
|
||||
force_str(res.content),
|
||||
{
|
||||
"name": "authentik Default Admin",
|
||||
"given_name": "authentik Default Admin",
|
||||
"family_name": "",
|
||||
"preferred_username": "akadmin",
|
||||
"nickname": "akadmin",
|
||||
"groups": ["authentik Admins"],
|
||||
"sub": "bar",
|
||||
},
|
||||
)
|
||||
self.assertEqual(res.status_code, 200)
|
||||
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.CONFIGURATION_ERROR,
|
||||
)
|
||||
self.assertTrue(events.exists())
|
||||
self.assertEqual(
|
||||
events.first().context["message"],
|
||||
"Failed to evaluate property-mapping: name 'q' is not defined",
|
||||
)
|
||||
@ -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,
|
||||
|
||||
@ -7,6 +7,8 @@ from django.http.response import HttpResponseBadRequest
|
||||
from django.views import View
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.providers.oauth2.constants import (
|
||||
SCOPE_GITHUB_ORG_READ,
|
||||
SCOPE_GITHUB_USER,
|
||||
@ -63,12 +65,20 @@ class UserInfoView(View):
|
||||
for scope in ScopeMapping.objects.filter(
|
||||
provider=token.provider, scope_name__in=scopes_from_client
|
||||
).order_by("scope_name"):
|
||||
value = scope.evaluate(
|
||||
user=token.user,
|
||||
request=self.request,
|
||||
provider=token.provider,
|
||||
token=token,
|
||||
)
|
||||
value = None
|
||||
try:
|
||||
value = scope.evaluate(
|
||||
user=token.user,
|
||||
request=self.request,
|
||||
provider=token.provider,
|
||||
token=token,
|
||||
)
|
||||
except PropertyMappingExpressionException as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping: {str(exc)}",
|
||||
mapping=scope,
|
||||
).from_http(self.request)
|
||||
if value is None:
|
||||
continue
|
||||
if not isinstance(value, dict):
|
||||
|
||||
@ -90,12 +90,6 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"""Proxy provider serializer for outposts"""
|
||||
|
||||
oidc_configuration = SerializerMethodField()
|
||||
forward_auth_mode = SerializerMethodField()
|
||||
|
||||
def get_forward_auth_mode(self, instance: ProxyProvider) -> bool:
|
||||
"""Legacy field for 2021.5 outposts"""
|
||||
# TODO: remove in 2021.7
|
||||
return instance.mode in [ProxyMode.FORWARD_SINGLE, ProxyMode.FORWARD_DOMAIN]
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -117,8 +111,6 @@ class ProxyOutpostConfigSerializer(ModelSerializer):
|
||||
"basic_auth_user_attribute",
|
||||
"mode",
|
||||
"cookie_domain",
|
||||
# Legacy field, remove in 2021.7
|
||||
"forward_auth_mode",
|
||||
]
|
||||
|
||||
@extend_schema_field(OpenIDConnectConfigurationSerializer)
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -113,6 +113,7 @@ class TraefikMiddlewareReconciler(KubernetesObjectReconciler[TraefikMiddleware])
|
||||
authResponseHeaders=[
|
||||
"Set-Cookie",
|
||||
"X-Auth-Username",
|
||||
"X-Auth-Groups",
|
||||
"X-Forwarded-Email",
|
||||
"X-Forwarded-Preferred-Username",
|
||||
"X-Forwarded-User",
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -9,6 +9,7 @@ from lxml.etree import Element, SubElement # nosec
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.lib.utils.time import timedelta_from_string
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequest
|
||||
@ -99,7 +100,11 @@ class AssertionProcessor:
|
||||
attribute_statement.append(attribute)
|
||||
|
||||
except PropertyMappingExpressionException as exc:
|
||||
LOGGER.warning(str(exc))
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping: {str(exc)}",
|
||||
mapping=mapping,
|
||||
).from_http(self.http_request)
|
||||
continue
|
||||
return attribute_statement
|
||||
|
||||
@ -161,7 +166,11 @@ class AssertionProcessor:
|
||||
name_id.text = value
|
||||
return name_id
|
||||
except PropertyMappingExpressionException as exc:
|
||||
LOGGER.warning(str(exc))
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping: {str(exc)}",
|
||||
mapping=self.provider.name_id_mapping,
|
||||
).from_http(self.http_request)
|
||||
return name_id
|
||||
if name_id.attrib["Format"] == SAML_NAME_ID_FORMAT_EMAIL:
|
||||
name_id.text = self.http_request.user.email
|
||||
|
||||
@ -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)"""
|
||||
|
||||
@ -6,9 +6,12 @@ from django.http.request import QueryDict
|
||||
from django.test import RequestFactory, TestCase
|
||||
from guardian.utils import get_anonymous_user
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.flows.models import Flow
|
||||
from authentik.flows.tests.test_planner import dummy_get_response
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.saml.models import SAMLPropertyMapping, SAMLProvider
|
||||
from authentik.providers.saml.processors.assertion import AssertionProcessor
|
||||
from authentik.providers.saml.processors.request_parser import AuthNRequestParser
|
||||
@ -79,6 +82,7 @@ class TestAuthNRequest(TestCase):
|
||||
"""Test AuthN Request generator and parser"""
|
||||
|
||||
def setUp(self):
|
||||
ObjectManager().run()
|
||||
cert = CertificateKeyPair.objects.first()
|
||||
self.provider: SAMLProvider = SAMLProvider.objects.create(
|
||||
authorization_flow=Flow.objects.get(
|
||||
@ -243,3 +247,60 @@ class TestAuthNRequest(TestCase):
|
||||
parsed_request = AuthNRequestParser(provider).parse(POST_REQUEST)
|
||||
self.assertEqual(parsed_request.id, "aws_LDxLGeubpc5lx12gxCgS6uPbix1yd5re")
|
||||
self.assertEqual(parsed_request.name_id_policy, SAML_NAME_ID_FORMAT_EMAIL)
|
||||
|
||||
def test_request_attributes(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
# To get an assertion we need a parsed request (parsed by provider)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
b64encode(request.encode()).decode(), "test_state"
|
||||
)
|
||||
# Now create a response and convert it to string (provider)
|
||||
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
|
||||
self.assertIn("akadmin", response_proc.build_response())
|
||||
|
||||
def test_request_attributes_invalid(self):
|
||||
"""Test full SAML Request/Response flow, fully signed"""
|
||||
http_request = self.factory.get("/")
|
||||
http_request.user = User.objects.get(username="akadmin")
|
||||
|
||||
middleware = SessionMiddleware(dummy_get_response)
|
||||
middleware.process_request(http_request)
|
||||
http_request.session.save()
|
||||
|
||||
# First create an AuthNRequest
|
||||
request_proc = RequestProcessor(self.source, http_request, "test_state")
|
||||
request = request_proc.build_auth_n()
|
||||
|
||||
# Create invalid PropertyMapping
|
||||
scope = SAMLPropertyMapping.objects.create(
|
||||
name="test", saml_name="test", expression="q"
|
||||
)
|
||||
self.provider.property_mappings.add(scope)
|
||||
|
||||
# To get an assertion we need a parsed request (parsed by provider)
|
||||
parsed_request = AuthNRequestParser(self.provider).parse(
|
||||
b64encode(request.encode()).decode(), "test_state"
|
||||
)
|
||||
# Now create a response and convert it to string (provider)
|
||||
response_proc = AssertionProcessor(self.provider, http_request, parsed_request)
|
||||
self.assertIn("akadmin", response_proc.build_response())
|
||||
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.CONFIGURATION_ERROR,
|
||||
)
|
||||
self.assertTrue(events.exists())
|
||||
self.assertEqual(
|
||||
events.first().context["message"],
|
||||
"Failed to evaluate property-mapping: name 'q' is not defined",
|
||||
)
|
||||
|
||||
@ -27,6 +27,7 @@ class ChannelsStorage(BaseStorage):
|
||||
uid,
|
||||
{
|
||||
"type": "event.update",
|
||||
"message_type": "message",
|
||||
"level": message.level_tag,
|
||||
"tags": message.tags,
|
||||
"message": message.message,
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -5,6 +5,7 @@ from django.db.models.query import QuerySet
|
||||
from structlog.stdlib import BoundLogger, get_logger
|
||||
|
||||
from authentik.core.exceptions import PropertyMappingExpressionException
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.sources.ldap.auth import LDAP_DISTINGUISHED_NAME
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
|
||||
@ -83,6 +84,11 @@ class BaseLDAPSynchronizer:
|
||||
else:
|
||||
properties[object_field] = self._flatten(value)
|
||||
except PropertyMappingExpressionException as exc:
|
||||
Event.new(
|
||||
EventAction.CONFIGURATION_ERROR,
|
||||
message=f"Failed to evaluate property-mapping: {str(exc)}",
|
||||
mapping=mapping,
|
||||
).save()
|
||||
self._logger.warning(
|
||||
"Mapping failed to evaluate", exc=exc, mapping=mapping
|
||||
)
|
||||
|
||||
@ -5,6 +5,7 @@ from django.db.models import Q
|
||||
from django.test import TestCase
|
||||
|
||||
from authentik.core.models import Group, User
|
||||
from authentik.events.models import Event, EventAction
|
||||
from authentik.managed.manager import ObjectManager
|
||||
from authentik.providers.oauth2.generators import generate_client_secret
|
||||
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||
@ -31,6 +32,33 @@ class LDAPSyncTests(TestCase):
|
||||
additional_group_dn="ou=groups",
|
||||
)
|
||||
|
||||
def test_sync_error(self):
|
||||
"""Test user sync"""
|
||||
self.source.property_mappings.set(
|
||||
LDAPPropertyMapping.objects.filter(
|
||||
Q(managed__startswith="goauthentik.io/sources/ldap/default")
|
||||
| Q(managed__startswith="goauthentik.io/sources/ldap/ms")
|
||||
)
|
||||
)
|
||||
mapping = LDAPPropertyMapping.objects.create(
|
||||
name="name",
|
||||
object_field="name",
|
||||
expression="q",
|
||||
)
|
||||
self.source.property_mappings.set([mapping])
|
||||
self.source.save()
|
||||
connection = PropertyMock(return_value=mock_ad_connection(LDAP_PASSWORD))
|
||||
with patch("authentik.sources.ldap.models.LDAPSource.connection", connection):
|
||||
user_sync = UserLDAPSynchronizer(self.source)
|
||||
user_sync.sync()
|
||||
self.assertFalse(User.objects.filter(username="user0_sn").exists())
|
||||
self.assertFalse(User.objects.filter(username="user1_sn").exists())
|
||||
events = Event.objects.filter(
|
||||
action=EventAction.CONFIGURATION_ERROR,
|
||||
context__message="Failed to evaluate property-mapping: name 'q' is not defined",
|
||||
)
|
||||
self.assertTrue(events.exists())
|
||||
|
||||
def test_sync_users_ad(self):
|
||||
"""Test user sync"""
|
||||
self.source.property_mappings.set(
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
@ -6,12 +6,6 @@ trigger:
|
||||
- next
|
||||
- version-*
|
||||
|
||||
variables:
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
|
||||
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], '/', '-') }}
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
|
||||
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
|
||||
|
||||
stages:
|
||||
- stage: generate
|
||||
jobs:
|
||||
|
||||
@ -14,13 +14,13 @@ resources:
|
||||
- repo: self
|
||||
|
||||
variables:
|
||||
POSTGRES_DB: authentik
|
||||
POSTGRES_USER: authentik
|
||||
POSTGRES_PASSWORD: "EK-5jnKfjrGRm<77"
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/pull/') }}:
|
||||
branchName: ${{ replace(variables['System.PullRequest.SourceBranch'], '/', '-') }}
|
||||
${{ if startsWith(variables['Build.SourceBranch'], 'refs/heads/') }}:
|
||||
branchName: ${{ replace(variables['Build.SourceBranchName'], 'refs/heads/', '') }}
|
||||
- name: POSTGRES_DB
|
||||
value: authentik
|
||||
- name: POSTGRES_USER
|
||||
value: authentik
|
||||
- name: POSTGRES_PASSWORD
|
||||
value: "EK-5jnKfjrGRm<77"
|
||||
- group: coverage
|
||||
|
||||
stages:
|
||||
- stage: Lint_and_test
|
||||
@ -397,6 +397,16 @@ stages:
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: bash <(curl -s https://codecov.io/bash)
|
||||
- task: CmdLine@2
|
||||
inputs:
|
||||
script: |
|
||||
npm install -g @zeus-ci/cli
|
||||
npx zeus job update -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -r $BUILD_SOURCEVERSION
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-cobertura+xml" coverage.xml
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-junit+xml" coverage-e2e/unittest.xml
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-junit+xml" coverage-integration/unittest.xml
|
||||
npx zeus upload -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -t "application/x-junit+xml" coverage-unittest/unittest.xml
|
||||
npx zeus job update --status=passed -b $BUILD_BUILDID -j $BUILD_BUILDNUMBER -r $BUILD_SOURCEVERSION
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: build_server
|
||||
|
||||
@ -21,7 +21,7 @@ services:
|
||||
networks:
|
||||
- internal
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.1-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.2}
|
||||
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-rc1}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2021.7.2}
|
||||
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-rc1"
|
||||
const VERSION = "2021.7.2"
|
||||
|
||||
@ -44,7 +44,7 @@ func NewAPIController(akURL url.URL, token string) *APIController {
|
||||
config.Host = akURL.Host
|
||||
config.Scheme = akURL.Scheme
|
||||
config.HTTPClient = &http.Client{
|
||||
Transport: GetTLSTransport(),
|
||||
Transport: NewTracingTransport(GetTLSTransport()),
|
||||
}
|
||||
config.AddDefaultHeader("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
|
||||
|
||||
@ -71,14 +71,12 @@ func (ac *APIController) Shutdown() {
|
||||
func (ac *APIController) startWSHandler() {
|
||||
logger := ac.logger.WithField("loop", "ws-handler")
|
||||
for {
|
||||
if !ac.wsConn.IsConnected() {
|
||||
continue
|
||||
}
|
||||
var wsMsg websocketMessage
|
||||
err := ac.wsConn.ReadJSON(&wsMsg)
|
||||
if err != nil {
|
||||
logger.WithError(err).Warning("ws write error, reconnecting")
|
||||
ac.wsConn.CloseAndReconnect()
|
||||
time.Sleep(time.Second * 5)
|
||||
continue
|
||||
}
|
||||
if wsMsg.Instruction == WebsocketInstructionTriggerUpdate {
|
||||
@ -118,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 {
|
||||
|
||||
85
internal/outpost/ak/crypto.go
Normal file
85
internal/outpost/ak/crypto.go
Normal file
@ -0,0 +1,85 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
type CryptoStore struct {
|
||||
api *api.CryptoApiService
|
||||
|
||||
log *log.Entry
|
||||
|
||||
fingerprints map[string]string
|
||||
certificates map[string]*tls.Certificate
|
||||
}
|
||||
|
||||
func NewCryptoStore(cryptoApi *api.CryptoApiService) *CryptoStore {
|
||||
return &CryptoStore{
|
||||
api: cryptoApi,
|
||||
log: log.WithField("logger", "authentik.outpost.cryptostore"),
|
||||
fingerprints: make(map[string]string),
|
||||
certificates: make(map[string]*tls.Certificate),
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *CryptoStore) AddKeypair(uuid string) error {
|
||||
// If they keypair was already added, don't
|
||||
// do it again
|
||||
if _, ok := cs.fingerprints[uuid]; ok {
|
||||
return nil
|
||||
}
|
||||
// reset fingerprint to force update
|
||||
cs.fingerprints[uuid] = ""
|
||||
err := cs.Fetch(uuid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cs.fingerprints[uuid] = cs.getFingerprint(uuid)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *CryptoStore) getFingerprint(uuid string) string {
|
||||
kp, _, err := cs.api.CryptoCertificatekeypairsRetrieve(context.Background(), uuid).Execute()
|
||||
if err != nil {
|
||||
cs.log.WithField("uuid", uuid).WithError(err).Warning("Failed to fetch certificate's fingerprint")
|
||||
return ""
|
||||
}
|
||||
return kp.FingerprintSha256
|
||||
}
|
||||
|
||||
func (cs *CryptoStore) Fetch(uuid string) error {
|
||||
cfp := cs.getFingerprint(uuid)
|
||||
if cfp == cs.fingerprints[uuid] {
|
||||
cs.log.WithField("uuid", uuid).Info("Fingerprint hasn't changed, not fetching cert")
|
||||
return nil
|
||||
}
|
||||
cs.log.WithField("uuid", uuid).Info("Fetching certificate and private key")
|
||||
|
||||
cert, _, err := cs.api.CryptoCertificatekeypairsViewCertificateRetrieve(context.Background(), uuid).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, _, err := cs.api.CryptoCertificatekeypairsViewPrivateKeyRetrieve(context.Background(), uuid).Execute()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
x509cert, err := tls.X509KeyPair([]byte(cert.Data), []byte(key.Data))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cs.certificates[uuid] = &x509cert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cs *CryptoStore) Get(uuid string) *tls.Certificate {
|
||||
err := cs.Fetch(uuid)
|
||||
if err != nil {
|
||||
cs.log.WithError(err).Warning("failed to fetch certificate")
|
||||
}
|
||||
return cs.certificates[uuid]
|
||||
}
|
||||
@ -1,8 +1,6 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
@ -11,7 +9,6 @@ import (
|
||||
"github.com/getsentry/sentry-go"
|
||||
httptransport "github.com/go-openapi/runtime/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
)
|
||||
|
||||
@ -38,15 +35,17 @@ func doGlobalSetup(config map[string]interface{}) {
|
||||
}
|
||||
log.WithField("buildHash", constants.BUILD()).WithField("version", constants.VERSION).Info("Starting authentik outpost")
|
||||
|
||||
env := config[ConfigErrorReportingEnvironment].(string)
|
||||
var dsn string
|
||||
if config[ConfigErrorReportingEnabled].(bool) {
|
||||
dsn = "https://a579bb09306d4f8b8d8847c052d3a1d3@sentry.beryju.org/8"
|
||||
log.Debug("Error reporting enabled")
|
||||
log.WithField("env", env).Debug("Error reporting enabled")
|
||||
}
|
||||
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: dsn,
|
||||
Environment: config[ConfigErrorReportingEnvironment].(string),
|
||||
Dsn: dsn,
|
||||
Environment: env,
|
||||
TracesSampleRate: 1,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("sentry.Init: %s", err)
|
||||
@ -69,21 +68,3 @@ func GetTLSTransport() http.RoundTripper {
|
||||
}
|
||||
return tlsTransport
|
||||
}
|
||||
|
||||
// ParseCertificate Load certificate from Keyepair UUID and parse it into a go Certificate
|
||||
func ParseCertificate(kpUuid string, cryptoApi *api.CryptoApiService) (*tls.Certificate, error) {
|
||||
cert, _, err := cryptoApi.CryptoCertificatekeypairsViewCertificateRetrieve(context.Background(), kpUuid).Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key, _, err := cryptoApi.CryptoCertificatekeypairsViewPrivateKeyRetrieve(context.Background(), kpUuid).Execute()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
x509cert, err := tls.X509KeyPair([]byte(cert.Data), []byte(key.Data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &x509cert, nil
|
||||
}
|
||||
|
||||
23
internal/outpost/ak/tracing.go
Normal file
23
internal/outpost/ak/tracing.go
Normal file
@ -0,0 +1,23 @@
|
||||
package ak
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
type tracingTransport struct {
|
||||
inner http.RoundTripper
|
||||
}
|
||||
|
||||
func NewTracingTransport(inner http.RoundTripper) *tracingTransport {
|
||||
return &tracingTransport{inner}
|
||||
}
|
||||
|
||||
func (tt *tracingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
span := sentry.StartSpan(r.Context(), "authentik.go.http_request")
|
||||
span.SetTag("url", r.URL.String())
|
||||
span.SetTag("method", r.Method)
|
||||
defer span.Finish()
|
||||
return tt.inner.RoundTrip(r.WithContext(span.Context()))
|
||||
}
|
||||
@ -11,6 +11,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/constants"
|
||||
@ -34,14 +35,19 @@ const (
|
||||
type FlowExecutor struct {
|
||||
Params url.Values
|
||||
Answers map[StageComponent]string
|
||||
Context context.Context
|
||||
|
||||
api *api.APIClient
|
||||
flowSlug string
|
||||
log *log.Entry
|
||||
token string
|
||||
|
||||
sp *sentry.Span
|
||||
}
|
||||
|
||||
func NewFlowExecutor(flowSlug string, refConfig *api.Configuration, logFields log.Fields) *FlowExecutor {
|
||||
func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Configuration, logFields log.Fields) *FlowExecutor {
|
||||
rsp := sentry.StartSpan(ctx, "authentik.outposts.flow_executor")
|
||||
|
||||
l := log.WithField("flow", flowSlug).WithFields(logFields)
|
||||
jar, err := cookiejar.New(nil)
|
||||
if err != nil {
|
||||
@ -55,16 +61,18 @@ func NewFlowExecutor(flowSlug string, refConfig *api.Configuration, logFields lo
|
||||
config.UserAgent = constants.OutpostUserAgent()
|
||||
config.HTTPClient = &http.Client{
|
||||
Jar: jar,
|
||||
Transport: ak.GetTLSTransport(),
|
||||
Transport: ak.NewTracingTransport(ak.GetTLSTransport()),
|
||||
}
|
||||
apiClient := api.NewAPIClient(config)
|
||||
return &FlowExecutor{
|
||||
Params: url.Values{},
|
||||
Answers: make(map[StageComponent]string),
|
||||
Context: rsp.Context(),
|
||||
api: apiClient,
|
||||
flowSlug: flowSlug,
|
||||
log: l,
|
||||
token: strings.Split(refConfig.DefaultHeader["Authorization"], " ")[1],
|
||||
sp: rsp,
|
||||
}
|
||||
}
|
||||
|
||||
@ -89,7 +97,9 @@ func (fe *FlowExecutor) DelegateClientIP(a net.Addr) {
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) CheckApplicationAccess(appSlug string) (bool, error) {
|
||||
p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(context.Background(), appSlug).Execute()
|
||||
acsp := sentry.StartSpan(fe.Context, "authentik.outposts.flow_executor.check_access")
|
||||
defer acsp.Finish()
|
||||
p, _, err := fe.api.CoreApi.CoreApplicationsCheckAccessRetrieve(acsp.Context(), appSlug).Execute()
|
||||
if !p.Passing {
|
||||
fe.log.Info("Access denied for user")
|
||||
return false, nil
|
||||
@ -113,14 +123,24 @@ func (fe *FlowExecutor) Execute() (bool, error) {
|
||||
}
|
||||
|
||||
func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) {
|
||||
req := fe.api.FlowsApi.FlowsExecutorGet(context.Background(), fe.flowSlug).Query(fe.Params.Encode())
|
||||
defer fe.sp.Finish()
|
||||
|
||||
// Get challenge
|
||||
gcsp := sentry.StartSpan(fe.Context, "authentik.outposts.flow_executor.get_challenge")
|
||||
req := fe.api.FlowsApi.FlowsExecutorGet(gcsp.Context(), fe.flowSlug).Query(fe.Params.Encode())
|
||||
challenge, _, err := req.Execute()
|
||||
if err != nil {
|
||||
return false, errors.New("failed to get challenge")
|
||||
}
|
||||
ch := challenge.GetActualInstance().(ChallengeInt)
|
||||
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got challenge")
|
||||
responseReq := fe.api.FlowsApi.FlowsExecutorSolve(context.Background(), fe.flowSlug).Query(fe.Params.Encode())
|
||||
gcsp.SetTag("ak_challenge", string(ch.GetType()))
|
||||
gcsp.SetTag("ak_component", ch.GetComponent())
|
||||
gcsp.Finish()
|
||||
|
||||
// Resole challenge
|
||||
scsp := sentry.StartSpan(fe.Context, "authentik.outposts.flow_executor.solve_challenge")
|
||||
responseReq := fe.api.FlowsApi.FlowsExecutorSolve(scsp.Context(), fe.flowSlug).Query(fe.Params.Encode())
|
||||
switch ch.GetComponent() {
|
||||
case string(StageIdentification):
|
||||
responseReq = responseReq.FlowChallengeResponseRequest(api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(api.NewIdentificationChallengeResponseRequest(fe.getAnswer(StageIdentification))))
|
||||
@ -150,9 +170,14 @@ func (fe *FlowExecutor) solveFlowChallenge(depth int) (bool, error) {
|
||||
default:
|
||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||
}
|
||||
|
||||
response, _, err := responseReq.Execute()
|
||||
ch = response.GetActualInstance().(ChallengeInt)
|
||||
fe.log.WithField("component", ch.GetComponent()).WithField("type", ch.GetType()).Debug("Got response")
|
||||
scsp.SetTag("ak_challenge", string(ch.GetType()))
|
||||
scsp.SetTag("ak_component", ch.GetComponent())
|
||||
scsp.Finish()
|
||||
|
||||
switch ch.GetComponent() {
|
||||
case string(StageAccessDenied):
|
||||
return false, errors.New("got ak-stage-access-denied")
|
||||
|
||||
@ -13,7 +13,6 @@ import (
|
||||
"github.com/go-openapi/strfmt"
|
||||
"github.com/pires/go-proxyproto"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
)
|
||||
|
||||
func (ls *LDAPServer) Refresh() error {
|
||||
@ -45,14 +44,12 @@ func (ls *LDAPServer) Refresh() error {
|
||||
gidStartNumber: *provider.GidStartNumber,
|
||||
}
|
||||
if provider.Certificate.Get() != nil {
|
||||
logger.WithField("provider", provider.Name).Debug("Enabling TLS")
|
||||
cert, err := ak.ParseCertificate(*provider.Certificate.Get(), ls.ac.Client.CryptoApi)
|
||||
kp := provider.Certificate.Get()
|
||||
err := ls.cs.AddKeypair(*kp)
|
||||
if err != nil {
|
||||
logger.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch certificate")
|
||||
} else {
|
||||
providers[idx].cert = cert
|
||||
logger.WithField("provider", provider.Name).Debug("Loaded certificates")
|
||||
ls.log.WithError(err).Warning("Failed to initially fetch certificate")
|
||||
}
|
||||
providers[idx].cert = ls.cs.Get(*kp)
|
||||
}
|
||||
}
|
||||
ls.providers = providers
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -15,9 +17,15 @@ type BindRequest struct {
|
||||
id string
|
||||
conn net.Conn
|
||||
log *log.Entry
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LDAPResultCode, error) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.bind",
|
||||
sentry.TransactionName("authentik.providers.ldap.bind"))
|
||||
span.SetTag("user.username", bindDN)
|
||||
defer span.Finish()
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
rid := uuid.New().String()
|
||||
req := BindRequest{
|
||||
@ -26,6 +34,7 @@ func (ls *LDAPServer) Bind(bindDN string, bindPW string, conn net.Conn) (ldap.LD
|
||||
conn: conn,
|
||||
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()),
|
||||
id: rid,
|
||||
ctx: span.Context(),
|
||||
}
|
||||
req.log.Info("Bind request")
|
||||
for _, instance := range ls.providers {
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/nmcclain/ldap"
|
||||
log "github.com/sirupsen/logrus"
|
||||
@ -34,7 +35,7 @@ func (pi *ProviderInstance) getUsername(dn string) (string, error) {
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPResultCode, error) {
|
||||
fe := outpost.NewFlowExecutor(pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
fe := outpost.NewFlowExecutor(req.ctx, pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||
"bindDN": req.BindDN,
|
||||
"client": req.conn.RemoteAddr().String(),
|
||||
"requestId": req.id,
|
||||
@ -53,6 +54,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
||||
req.log.WithError(err).Warning("failed to execute flow")
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
|
||||
access, err := fe.CheckApplicationAccess(pi.appSlug)
|
||||
if !access {
|
||||
req.log.Info("Access denied for user")
|
||||
@ -63,6 +65,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
||||
return ldap.LDAPResultOperationsError, nil
|
||||
}
|
||||
req.log.Info("User has access")
|
||||
uisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.bind.user_info")
|
||||
// Get user info to store in context
|
||||
userInfo, _, err := fe.ApiClient().CoreApi.CoreUsersMeRetrieve(context.Background()).Execute()
|
||||
if err != nil {
|
||||
@ -78,7 +81,7 @@ func (pi *ProviderInstance) Bind(username string, req BindRequest) (ldap.LDAPRes
|
||||
if pi.boundUsers[req.BindDN].CanSearch {
|
||||
req.log.WithField("group", cs).Info("Allowed access to search")
|
||||
}
|
||||
|
||||
uisp.Finish()
|
||||
defer pi.boundUsersMutex.Unlock()
|
||||
pi.delayDeleteUserInfo(username)
|
||||
return ldap.LDAPResultSuccess, nil
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
@ -17,6 +18,7 @@ func (pi *ProviderInstance) SearchMe(user api.User) (ldap.ServerSearchResult, er
|
||||
}
|
||||
|
||||
func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult, error) {
|
||||
accsp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.check_access")
|
||||
baseDN := strings.ToLower("," + pi.BaseDN)
|
||||
|
||||
entries := []*ldap.Entry{}
|
||||
@ -32,8 +34,8 @@ func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult,
|
||||
}
|
||||
|
||||
pi.boundUsersMutex.RLock()
|
||||
defer pi.boundUsersMutex.RUnlock()
|
||||
flags, ok := pi.boundUsers[req.BindDN]
|
||||
pi.boundUsersMutex.RUnlock()
|
||||
if !ok {
|
||||
pi.log.Debug("User info not cached")
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultInsufficientAccessRights}, errors.New("access denied")
|
||||
@ -42,31 +44,60 @@ func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult,
|
||||
pi.log.Debug("User can't search, showing info about user")
|
||||
return pi.SearchMe(flags.UserInfo)
|
||||
}
|
||||
accsp.Finish()
|
||||
|
||||
parsedFilter, err := ldap.CompileFilter(req.Filter)
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: error parsing filter: %s", req.Filter)
|
||||
}
|
||||
|
||||
switch filterEntity {
|
||||
default:
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("Search Error: unhandled filter type: %s [%s]", filterEntity, req.Filter)
|
||||
case GroupObjectClass:
|
||||
groups, _, err := pi.s.ac.Client.CoreApi.CoreGroupsList(context.Background()).Execute()
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
|
||||
wg := sync.WaitGroup{}
|
||||
wg.Add(2)
|
||||
|
||||
for _, g := range groups.Results {
|
||||
entries = append(entries, pi.GroupEntry(pi.APIGroupToLDAPGroup(g)))
|
||||
}
|
||||
gEntries := make([]*ldap.Entry, 0)
|
||||
uEntries := make([]*ldap.Entry, 0)
|
||||
|
||||
users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute()
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
gapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_group")
|
||||
groups, _, err := parseFilterForGroup(pi.s.ac.Client.CoreApi.CoreGroupsList(gapisp.Context()), parsedFilter).Execute()
|
||||
gapisp.Finish()
|
||||
if err != nil {
|
||||
req.log.WithError(err).Warning("failed to get groups")
|
||||
return
|
||||
}
|
||||
pi.log.WithField("count", len(groups.Results)).Trace("Got results from API")
|
||||
|
||||
for _, u := range users.Results {
|
||||
entries = append(entries, pi.GroupEntry(pi.APIUserToLDAPGroup(u)))
|
||||
}
|
||||
for _, g := range groups.Results {
|
||||
gEntries = append(gEntries, pi.GroupEntry(pi.APIGroupToLDAPGroup(g)))
|
||||
}
|
||||
}()
|
||||
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
|
||||
users, _, err := parseFilterForUser(pi.s.ac.Client.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter).Execute()
|
||||
uapisp.Finish()
|
||||
if err != nil {
|
||||
req.log.WithError(err).Warning("failed to get groups")
|
||||
return
|
||||
}
|
||||
|
||||
for _, u := range users.Results {
|
||||
uEntries = append(uEntries, pi.GroupEntry(pi.APIUserToLDAPGroup(u)))
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
entries = append(gEntries, uEntries...)
|
||||
case UserObjectClass, "":
|
||||
users, _, err := pi.s.ac.Client.CoreApi.CoreUsersList(context.Background()).Execute()
|
||||
uapisp := sentry.StartSpan(req.ctx, "authentik.providers.ldap.search.api_user")
|
||||
users, _, err := parseFilterForUser(pi.s.ac.Client.CoreApi.CoreUsersList(uapisp.Context()), parsedFilter).Execute()
|
||||
uapisp.Finish()
|
||||
|
||||
if err != nil {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultOperationsError}, fmt.Errorf("API Error: %s", err)
|
||||
}
|
||||
@ -74,7 +105,6 @@ func (pi *ProviderInstance) Search(req SearchRequest) (ldap.ServerSearchResult,
|
||||
entries = append(entries, pi.UserEntry(u))
|
||||
}
|
||||
}
|
||||
pi.log.WithField("filter", req.Filter).Debug("Search OK")
|
||||
return ldap.ServerSearchResult{Entries: entries, Referrals: []string{}, Controls: []ldap.Control{}, ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
}
|
||||
|
||||
|
||||
57
internal/outpost/ldap/instance_search_group.go
Normal file
57
internal/outpost/ldap/instance_search_group.go
Normal file
@ -0,0 +1,57 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
ber "github.com/nmcclain/asn1-ber"
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func parseFilterForGroup(req api.ApiCoreGroupsListRequest, f *ber.Packet) api.ApiCoreGroupsListRequest {
|
||||
switch f.Tag {
|
||||
case ldap.FilterEqualityMatch:
|
||||
return parseFilterForGroupSingle(req, f)
|
||||
case ldap.FilterAnd:
|
||||
for _, child := range f.Children {
|
||||
req = parseFilterForGroup(req, child)
|
||||
}
|
||||
return req
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func parseFilterForGroupSingle(req api.ApiCoreGroupsListRequest, f *ber.Packet) api.ApiCoreGroupsListRequest {
|
||||
// We can only handle key = value pairs here
|
||||
if len(f.Children) < 2 {
|
||||
return req
|
||||
}
|
||||
k := f.Children[0].Value
|
||||
// Ensure key is string
|
||||
if _, ok := k.(string); !ok {
|
||||
return req
|
||||
}
|
||||
v := f.Children[1].Value
|
||||
// Null values are ignored
|
||||
if v == nil {
|
||||
return req
|
||||
}
|
||||
// Switch on type of the value, then check the key
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
switch k {
|
||||
case "cn":
|
||||
return req.Name(vv)
|
||||
case "member":
|
||||
userDN, err := goldap.ParseDN(vv)
|
||||
if err != nil {
|
||||
return req
|
||||
}
|
||||
username := userDN.RDNs[0].Attributes[0].Value
|
||||
return req.MembersByUsername([]string{username})
|
||||
}
|
||||
// TODO: Support int
|
||||
default:
|
||||
return req
|
||||
}
|
||||
return req
|
||||
}
|
||||
54
internal/outpost/ldap/instance_search_user.go
Normal file
54
internal/outpost/ldap/instance_search_user.go
Normal file
@ -0,0 +1,54 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
ber "github.com/nmcclain/asn1-ber"
|
||||
"github.com/nmcclain/ldap"
|
||||
"goauthentik.io/api"
|
||||
)
|
||||
|
||||
func parseFilterForUser(req api.ApiCoreUsersListRequest, f *ber.Packet) api.ApiCoreUsersListRequest {
|
||||
switch f.Tag {
|
||||
case ldap.FilterEqualityMatch:
|
||||
return parseFilterForUserSingle(req, f)
|
||||
case ldap.FilterAnd:
|
||||
for _, child := range f.Children {
|
||||
req = parseFilterForUser(req, child)
|
||||
}
|
||||
return req
|
||||
}
|
||||
return req
|
||||
}
|
||||
|
||||
func parseFilterForUserSingle(req api.ApiCoreUsersListRequest, f *ber.Packet) api.ApiCoreUsersListRequest {
|
||||
// We can only handle key = value pairs here
|
||||
if len(f.Children) < 2 {
|
||||
return req
|
||||
}
|
||||
k := f.Children[0].Value
|
||||
// Ensure key is string
|
||||
if _, ok := k.(string); !ok {
|
||||
return req
|
||||
}
|
||||
v := f.Children[1].Value
|
||||
// Null values are ignored
|
||||
if v == nil {
|
||||
return req
|
||||
}
|
||||
// Switch on type of the value, then check the key
|
||||
switch vv := v.(type) {
|
||||
case string:
|
||||
switch k {
|
||||
case "cn":
|
||||
return req.Username(vv)
|
||||
case "name":
|
||||
case "displayName":
|
||||
return req.Name(vv)
|
||||
case "mail":
|
||||
return req.Email(vv)
|
||||
}
|
||||
// TODO: Support int
|
||||
default:
|
||||
return req
|
||||
}
|
||||
return req
|
||||
}
|
||||
@ -47,6 +47,7 @@ type LDAPServer struct {
|
||||
s *ldap.Server
|
||||
log *log.Entry
|
||||
ac *ak.APIController
|
||||
cs *ak.CryptoStore
|
||||
defaultCert *tls.Certificate
|
||||
providers []*ProviderInstance
|
||||
}
|
||||
@ -69,6 +70,7 @@ func NewServer(ac *ak.APIController) *LDAPServer {
|
||||
s: s,
|
||||
log: log.WithField("logger", "authentik.outpost.ldap"),
|
||||
ac: ac,
|
||||
cs: ak.NewCryptoStore(ac.Client.CryptoApi),
|
||||
providers: []*ProviderInstance{},
|
||||
}
|
||||
defaultCert, err := crypto.GenerateSelfSignedCert()
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"strings"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
goldap "github.com/go-ldap/ldap/v3"
|
||||
"github.com/google/uuid"
|
||||
"github.com/nmcclain/ldap"
|
||||
@ -17,9 +19,15 @@ type SearchRequest struct {
|
||||
id string
|
||||
conn net.Conn
|
||||
log *log.Entry
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (ldap.ServerSearchResult, error) {
|
||||
span := sentry.StartSpan(context.TODO(), "authentik.providers.ldap.search", sentry.TransactionName("authentik.providers.ldap.search"))
|
||||
span.SetTag("user.username", bindDN)
|
||||
span.SetTag("ak_filter", searchReq.Filter)
|
||||
span.SetTag("ak_base_dn", searchReq.BaseDN)
|
||||
|
||||
bindDN = strings.ToLower(bindDN)
|
||||
rid := uuid.New().String()
|
||||
req := SearchRequest{
|
||||
@ -28,8 +36,22 @@ func (ls *LDAPServer) Search(bindDN string, searchReq ldap.SearchRequest, conn n
|
||||
conn: conn,
|
||||
log: ls.log.WithField("bindDN", bindDN).WithField("requestId", rid).WithField("client", conn.RemoteAddr().String()).WithField("filter", searchReq.Filter).WithField("baseDN", searchReq.BaseDN),
|
||||
id: rid,
|
||||
ctx: span.Context(),
|
||||
}
|
||||
req.log.Info("Search request")
|
||||
|
||||
defer func() {
|
||||
span.Finish()
|
||||
req.log.WithField("took-ms", span.EndTime.Sub(span.StartTime).Milliseconds()).Info("Search request")
|
||||
}()
|
||||
|
||||
defer func() {
|
||||
err := recover()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
log.WithError(err.(error)).Error("recover in serach request")
|
||||
sentry.CaptureException(err.(error))
|
||||
}()
|
||||
|
||||
if searchReq.BaseDN == "" {
|
||||
return ldap.ServerSearchResult{ResultCode: ldap.LDAPResultSuccess}, nil
|
||||
|
||||
@ -15,7 +15,6 @@ import (
|
||||
"github.com/oauth2-proxy/oauth2-proxy/pkg/validation"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"goauthentik.io/api"
|
||||
"goauthentik.io/internal/outpost/ak"
|
||||
)
|
||||
|
||||
type providerBundle struct {
|
||||
@ -89,14 +88,12 @@ func (pb *providerBundle) prepareOpts(provider api.ProxyOutpostConfig) *options.
|
||||
}
|
||||
|
||||
if provider.Certificate.Get() != nil {
|
||||
pb.log.WithField("provider", provider.Name).Debug("Enabling TLS")
|
||||
cert, err := ak.ParseCertificate(*provider.Certificate.Get(), pb.s.ak.Client.CryptoApi)
|
||||
kp := provider.Certificate.Get()
|
||||
err := pb.s.cs.AddKeypair(*kp)
|
||||
if err != nil {
|
||||
pb.log.WithField("provider", provider.Name).WithError(err).Warning("Failed to fetch certificate")
|
||||
return providerOpts
|
||||
pb.log.WithError(err).Warning("Failed to initially fetch certificate")
|
||||
}
|
||||
pb.cert = cert
|
||||
pb.log.WithField("provider", provider.Name).Debug("Loaded certificates")
|
||||
pb.cert = pb.s.cs.Get(*kp)
|
||||
}
|
||||
return providerOpts
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ type Claims struct {
|
||||
Proxy struct {
|
||||
UserAttributes map[string]interface{} `json:"user_attributes"`
|
||||
} `json:"ak_proxy"`
|
||||
Groups []string `json:"groups"`
|
||||
}
|
||||
|
||||
func (c *Claims) FromIDToken(idToken string) error {
|
||||
|
||||
@ -428,6 +428,10 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
|
||||
if err != nil {
|
||||
log.WithError(err).Warning("Failed to parse IDToken")
|
||||
}
|
||||
// Set groups in header
|
||||
groups := strings.Join(claims.Groups, "|")
|
||||
req.Header["X-Auth-Groups"] = []string{groups}
|
||||
|
||||
userAttributes := claims.Proxy.UserAttributes
|
||||
// Attempt to set basic auth based on user's attributes
|
||||
if p.SetBasicAuth {
|
||||
@ -461,6 +465,7 @@ func (p *OAuthProxy) addHeadersForProxying(rw http.ResponseWriter, req *http.Req
|
||||
func (p *OAuthProxy) stripAuthHeaders(req *http.Request) {
|
||||
if p.PassUserHeaders {
|
||||
req.Header.Del("X-Forwarded-User")
|
||||
req.Header.Del("X-Auth-Groups")
|
||||
req.Header.Del("X-Forwarded-Email")
|
||||
req.Header.Del("X-Forwarded-Preferred-Username")
|
||||
}
|
||||
|
||||
@ -21,6 +21,7 @@ type Server struct {
|
||||
stop chan struct{} // channel for waiting shutdown
|
||||
logger *log.Entry
|
||||
ak *ak.APIController
|
||||
cs *ak.CryptoStore
|
||||
defaultCert tls.Certificate
|
||||
}
|
||||
|
||||
@ -35,6 +36,7 @@ func NewServer(ac *ak.APIController) *Server {
|
||||
logger: log.WithField("logger", "authentik.outpost.proxy-http-server"),
|
||||
defaultCert: defaultCert,
|
||||
ak: ac,
|
||||
cs: ak.NewCryptoStore(ac.Client.CryptoApi),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -15,8 +15,6 @@ RUN docker-entrypoint.sh generate \
|
||||
|
||||
# Stage 2: Build
|
||||
FROM golang:1.16.6 AS builder
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -28,6 +26,9 @@ RUN go build -o /go/ldap ./cmd/ldap
|
||||
# Stage 3: Run
|
||||
FROM gcr.io/distroless/base-debian10:debug
|
||||
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
COPY --from=builder /go/ldap /
|
||||
|
||||
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:4180/akprox/ping" ]
|
||||
|
||||
@ -15,8 +15,6 @@ RUN docker-entrypoint.sh generate \
|
||||
|
||||
# Stage 2: Build
|
||||
FROM golang:1.16.6 AS builder
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
WORKDIR /go/src/goauthentik.io
|
||||
|
||||
@ -28,6 +26,9 @@ RUN go build -o /go/proxy ./cmd/proxy
|
||||
# Stage 3: Run
|
||||
FROM gcr.io/distroless/base-debian10:debug
|
||||
|
||||
ARG GIT_BUILD_HASH
|
||||
ENV GIT_BUILD_HASH=$GIT_BUILD_HASH
|
||||
|
||||
COPY --from=builder /go/proxy /
|
||||
|
||||
HEALTHCHECK CMD [ "wget", "--spider", "http://localhost:4180/akprox/ping" ]
|
||||
|
||||
38
schema.yml
38
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2021.7.1-rc1
|
||||
version: 2021.7.2
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@beryju.org
|
||||
@ -1932,6 +1932,24 @@ paths:
|
||||
name: is_superuser
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: members_by_pk
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: integer
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: members_by_username
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
|
||||
only.
|
||||
explode: true
|
||||
style: form
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
@ -2843,6 +2861,10 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
description: Attributes
|
||||
- in: query
|
||||
name: email
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: is_active
|
||||
schema:
|
||||
@ -25880,6 +25902,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
|
||||
@ -26694,13 +26719,8 @@ components:
|
||||
Exclusive with internal_host.
|
||||
cookie_domain:
|
||||
type: string
|
||||
forward_auth_mode:
|
||||
type: boolean
|
||||
readOnly: true
|
||||
deprecated: true
|
||||
required:
|
||||
- external_host
|
||||
- forward_auth_mode
|
||||
- name
|
||||
- oidc_configuration
|
||||
- pk
|
||||
@ -27960,6 +27980,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
|
||||
@ -27995,6 +28018,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:
|
||||
|
||||
@ -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")
|
||||
|
||||
334
web/package-lock.json
generated
334
web/package-lock.json
generated
@ -24,20 +24,20 @@
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-replace": "^3.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.3",
|
||||
"@sentry/browser": "^6.9.0",
|
||||
"@sentry/tracing": "^6.9.0",
|
||||
"@sentry/browser": "^6.10.0",
|
||||
"@sentry/tracing": "^6.10.0",
|
||||
"@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.1",
|
||||
"codemirror": "^5.62.2",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
@ -48,7 +48,7 @@
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.53.2",
|
||||
"rollup": "^2.54.0",
|
||||
"rollup-plugin-commonjs": "^10.1.0",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
"rollup-plugin-cssimport": "^1.0.2",
|
||||
@ -2341,13 +2341,13 @@
|
||||
"integrity": "sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg=="
|
||||
},
|
||||
"node_modules/@sentry/browser": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.9.0.tgz",
|
||||
"integrity": "sha512-4JnEPcwoNs6JqeEd4wscBq+hxpotEJ0DJ4eOIsaNZIMyqEHXBHTXCk/gfrSsiZFrkHM4PgvUHOxaC0HcZ92oBA==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.10.0.tgz",
|
||||
"integrity": "sha512-H0Blgp8f8bomebkkGWIgxHVjabtQAlsKJDiFXBg7gIc75YcarRxwH0R3hMog1/h8mmv4CGGUsy5ljYW6jsNnvA==",
|
||||
"dependencies": {
|
||||
"@sentry/core": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/core": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2360,14 +2360,14 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/core": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.9.0.tgz",
|
||||
"integrity": "sha512-oFX2qQcMLujCeIuCQGlhpTUIOXiU5n6V2lqDnvMXUV8gKpplBPalwdlR9bgbSi+VO8u7LjHR1IKM0RAPWgNHWw==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.10.0.tgz",
|
||||
"integrity": "sha512-5KlxHJlbD7AMo+b9pMGkjxUOfMILtsqCtGgI7DMvZNfEkdohO8QgUY+hPqr540kmwArFS91ipQYWhqzGaOhM3Q==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.9.0",
|
||||
"@sentry/minimal": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/hub": "6.10.0",
|
||||
"@sentry/minimal": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2380,12 +2380,12 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/hub": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.9.0.tgz",
|
||||
"integrity": "sha512-5mors7ojbo7G85ZmoVPQBgFBMONAJwyZfV0LNLy14GenoaVNuxTPyvAQiJb1FYq+x6YZ3CvqGX6r74KRKQU87w==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.10.0.tgz",
|
||||
"integrity": "sha512-MV8wjhWiFAXZAhmj7Ef5QdBr2IF93u8xXiIo2J+dRZ7eVa4/ZszoUiDbhUcl/TPxczaw4oW2a6tINBNFLzXiig==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2398,12 +2398,12 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/minimal": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.9.0.tgz",
|
||||
"integrity": "sha512-GBZ6wG2Rc1wInYEl2BZTZc/t57O1Da876ifLsSPpEQAEnGWbqZWb8RLjZskH09ZIL/K4XCIDDi5ySzN8kFUWJw==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.10.0.tgz",
|
||||
"integrity": "sha512-yarm046UgUFIBoxqnBan2+BEgaO9KZCrLzsIsmALiQvpfW92K1lHurSawl5W6SR7wCYBnNn7CPvPE/BHFdy4YA==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/hub": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2416,14 +2416,14 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/tracing": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.9.0.tgz",
|
||||
"integrity": "sha512-gogVTypolhPazXr3Lue8HgzBg5Sy1cQpEp5Iq9LtECs+TlOlxJ+S+P+EIjEZ0f1AHVu706jr5cY2G2Shluli9g==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.10.0.tgz",
|
||||
"integrity": "sha512-jZj6Aaf8kU5wgyNXbAJHosHn8OOFdK14lgwYPb/AIDsY35g9a9ncTOqIOBp8X3KkmSR8lcBzAEyiUzCxAis2jA==",
|
||||
"dependencies": {
|
||||
"@sentry/hub": "6.9.0",
|
||||
"@sentry/minimal": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/hub": "6.10.0",
|
||||
"@sentry/minimal": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -2436,19 +2436,19 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="
|
||||
},
|
||||
"node_modules/@sentry/types": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.9.0.tgz",
|
||||
"integrity": "sha512-v52HJqLoLapEnqS2NdVtUXPvT+aezQgNXQkp8hiQ3RUdTm5cffwBVG7wlbpE6OsOOIZxd6p1zKylFkwCypiIIA==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.10.0.tgz",
|
||||
"integrity": "sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@sentry/utils": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.9.0.tgz",
|
||||
"integrity": "sha512-PimDr6KAi4cCp5hQZ8Az2/pDcdfhTu7WAU30Dd9MZwknpHSTmD4G6QvkdrB5er6kMMnNQOC7rMo6w/Do3m6X3w==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.10.0.tgz",
|
||||
"integrity": "sha512-F9OczOcZMFtazYVZ6LfRIe65/eOfQbiAedIKS0li4npuMz0jKYRbxrjd/U7oLiNQkPAp4/BujU4m1ZIwq6a+tg==",
|
||||
"dependencies": {
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"engines": {
|
||||
@ -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",
|
||||
@ -3502,9 +3502,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/codemirror": {
|
||||
"version": "5.62.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.1.tgz",
|
||||
"integrity": "sha512-39ce8tHh/M9J+Epa90R5zMGg06pxVXc1+Y0SRR6eKaUjjzuj5iYkk7rHc2uU+FzvfsWYGEYKPFf0pBVBLmYXNQ=="
|
||||
"version": "5.62.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz",
|
||||
"integrity": "sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw=="
|
||||
},
|
||||
"node_modules/collection-visit": {
|
||||
"version": "1.0.0",
|
||||
@ -6790,9 +6790,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.53.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.53.2.tgz",
|
||||
"integrity": "sha512-1CtEYuS5CRCzFZ7SNW5528SlDlk4VDXIRGwbm/2POQxA/G4+7/crIqJwkmnj8Q/74hGx4oVlNvh4E1CJQ5hZ6w==",
|
||||
"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"
|
||||
},
|
||||
@ -9716,13 +9716,13 @@
|
||||
}
|
||||
},
|
||||
"@sentry/browser": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.9.0.tgz",
|
||||
"integrity": "sha512-4JnEPcwoNs6JqeEd4wscBq+hxpotEJ0DJ4eOIsaNZIMyqEHXBHTXCk/gfrSsiZFrkHM4PgvUHOxaC0HcZ92oBA==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-6.10.0.tgz",
|
||||
"integrity": "sha512-H0Blgp8f8bomebkkGWIgxHVjabtQAlsKJDiFXBg7gIc75YcarRxwH0R3hMog1/h8mmv4CGGUsy5ljYW6jsNnvA==",
|
||||
"requires": {
|
||||
"@sentry/core": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/core": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9734,14 +9734,14 @@
|
||||
}
|
||||
},
|
||||
"@sentry/core": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.9.0.tgz",
|
||||
"integrity": "sha512-oFX2qQcMLujCeIuCQGlhpTUIOXiU5n6V2lqDnvMXUV8gKpplBPalwdlR9bgbSi+VO8u7LjHR1IKM0RAPWgNHWw==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-6.10.0.tgz",
|
||||
"integrity": "sha512-5KlxHJlbD7AMo+b9pMGkjxUOfMILtsqCtGgI7DMvZNfEkdohO8QgUY+hPqr540kmwArFS91ipQYWhqzGaOhM3Q==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.9.0",
|
||||
"@sentry/minimal": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/hub": "6.10.0",
|
||||
"@sentry/minimal": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9753,12 +9753,12 @@
|
||||
}
|
||||
},
|
||||
"@sentry/hub": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.9.0.tgz",
|
||||
"integrity": "sha512-5mors7ojbo7G85ZmoVPQBgFBMONAJwyZfV0LNLy14GenoaVNuxTPyvAQiJb1FYq+x6YZ3CvqGX6r74KRKQU87w==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-6.10.0.tgz",
|
||||
"integrity": "sha512-MV8wjhWiFAXZAhmj7Ef5QdBr2IF93u8xXiIo2J+dRZ7eVa4/ZszoUiDbhUcl/TPxczaw4oW2a6tINBNFLzXiig==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9770,12 +9770,12 @@
|
||||
}
|
||||
},
|
||||
"@sentry/minimal": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.9.0.tgz",
|
||||
"integrity": "sha512-GBZ6wG2Rc1wInYEl2BZTZc/t57O1Da876ifLsSPpEQAEnGWbqZWb8RLjZskH09ZIL/K4XCIDDi5ySzN8kFUWJw==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-6.10.0.tgz",
|
||||
"integrity": "sha512-yarm046UgUFIBoxqnBan2+BEgaO9KZCrLzsIsmALiQvpfW92K1lHurSawl5W6SR7wCYBnNn7CPvPE/BHFdy4YA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/hub": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9787,14 +9787,14 @@
|
||||
}
|
||||
},
|
||||
"@sentry/tracing": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.9.0.tgz",
|
||||
"integrity": "sha512-gogVTypolhPazXr3Lue8HgzBg5Sy1cQpEp5Iq9LtECs+TlOlxJ+S+P+EIjEZ0f1AHVu706jr5cY2G2Shluli9g==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/tracing/-/tracing-6.10.0.tgz",
|
||||
"integrity": "sha512-jZj6Aaf8kU5wgyNXbAJHosHn8OOFdK14lgwYPb/AIDsY35g9a9ncTOqIOBp8X3KkmSR8lcBzAEyiUzCxAis2jA==",
|
||||
"requires": {
|
||||
"@sentry/hub": "6.9.0",
|
||||
"@sentry/minimal": "6.9.0",
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/utils": "6.9.0",
|
||||
"@sentry/hub": "6.10.0",
|
||||
"@sentry/minimal": "6.10.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"@sentry/utils": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -9806,16 +9806,16 @@
|
||||
}
|
||||
},
|
||||
"@sentry/types": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.9.0.tgz",
|
||||
"integrity": "sha512-v52HJqLoLapEnqS2NdVtUXPvT+aezQgNXQkp8hiQ3RUdTm5cffwBVG7wlbpE6OsOOIZxd6p1zKylFkwCypiIIA=="
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/types/-/types-6.10.0.tgz",
|
||||
"integrity": "sha512-M7s0JFgG7/6/yNVYoPUbxzaXDhnzyIQYRRJJKRaTD77YO4MHvi4Ke8alBWqD5fer0cPIfcSkBqa9BLdqRqcMWw=="
|
||||
},
|
||||
"@sentry/utils": {
|
||||
"version": "6.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.9.0.tgz",
|
||||
"integrity": "sha512-PimDr6KAi4cCp5hQZ8Az2/pDcdfhTu7WAU30Dd9MZwknpHSTmD4G6QvkdrB5er6kMMnNQOC7rMo6w/Do3m6X3w==",
|
||||
"version": "6.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-6.10.0.tgz",
|
||||
"integrity": "sha512-F9OczOcZMFtazYVZ6LfRIe65/eOfQbiAedIKS0li4npuMz0jKYRbxrjd/U7oLiNQkPAp4/BujU4m1ZIwq6a+tg==",
|
||||
"requires": {
|
||||
"@sentry/types": "6.9.0",
|
||||
"@sentry/types": "6.10.0",
|
||||
"tslib": "^1.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
@ -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",
|
||||
@ -10636,9 +10636,9 @@
|
||||
"integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4="
|
||||
},
|
||||
"codemirror": {
|
||||
"version": "5.62.1",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.1.tgz",
|
||||
"integrity": "sha512-39ce8tHh/M9J+Epa90R5zMGg06pxVXc1+Y0SRR6eKaUjjzuj5iYkk7rHc2uU+FzvfsWYGEYKPFf0pBVBLmYXNQ=="
|
||||
"version": "5.62.2",
|
||||
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.62.2.tgz",
|
||||
"integrity": "sha512-tVFMUa4J3Q8JUd1KL9yQzQB0/BJt7ZYZujZmTPgo/54Lpuq3ez4C8x/ATUY/wv7b7X3AUq8o3Xd+2C5ZrCGWHw=="
|
||||
},
|
||||
"collection-visit": {
|
||||
"version": "1.0.0",
|
||||
@ -13241,9 +13241,9 @@
|
||||
}
|
||||
},
|
||||
"rollup": {
|
||||
"version": "2.53.2",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.53.2.tgz",
|
||||
"integrity": "sha512-1CtEYuS5CRCzFZ7SNW5528SlDlk4VDXIRGwbm/2POQxA/G4+7/crIqJwkmnj8Q/74hGx4oVlNvh4E1CJQ5hZ6w==",
|
||||
"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"
|
||||
}
|
||||
|
||||
@ -53,20 +53,20 @@
|
||||
"@rollup/plugin-babel": "^5.3.0",
|
||||
"@rollup/plugin-replace": "^3.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.3",
|
||||
"@sentry/browser": "^6.9.0",
|
||||
"@sentry/tracing": "^6.9.0",
|
||||
"@sentry/browser": "^6.10.0",
|
||||
"@sentry/tracing": "^6.10.0",
|
||||
"@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.1",
|
||||
"codemirror": "^5.62.2",
|
||||
"construct-style-sheets-polyfill": "^2.4.16",
|
||||
"eslint": "^7.31.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
@ -77,7 +77,7 @@
|
||||
"lit-html": "^1.4.1",
|
||||
"moment": "^2.29.1",
|
||||
"rapidoc": "^9.0.0",
|
||||
"rollup": "^2.53.2",
|
||||
"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 => {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
--ak-dark-background: #18191a;
|
||||
--ak-dark-background-darker: #000000;
|
||||
--ak-dark-background-light: #1c1e21;
|
||||
--ak-dark-background-light-ish: #212427;
|
||||
--ak-dark-background-lighter: #2b2e33;
|
||||
}
|
||||
|
||||
@ -127,7 +128,8 @@ body {
|
||||
.pf-c-page__main-section {
|
||||
--pf-c-page__main-section--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
.sidebar-trigger {
|
||||
.sidebar-trigger,
|
||||
.notification-trigger {
|
||||
background-color: var(--ak-dark-background-light) !important;
|
||||
}
|
||||
.pf-c-page__main-section.pf-m-light {
|
||||
@ -302,6 +304,9 @@ body {
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* notifications */
|
||||
.pf-c-drawer__panel {
|
||||
background-color: var(--ak-dark-background);
|
||||
}
|
||||
.pf-c-notification-drawer {
|
||||
--pf-c-notification-drawer--BackgroundColor: var(--ak-dark-background);
|
||||
}
|
||||
@ -309,6 +314,10 @@ body {
|
||||
background-color: var(--ak-dark-background-lighter);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
.pf-c-notification-drawer__list-item {
|
||||
background-color: var(--ak-dark-background-light-ish);
|
||||
color: var(--ak-dark-foreground);
|
||||
}
|
||||
/* data list */
|
||||
.pf-c-data-list__item {
|
||||
--pf-c-data-list__item--BackgroundColor: var(--ak-dark-background-light);
|
||||
|
||||
61
web/src/common/ws.ts
Normal file
61
web/src/common/ws.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { EVENT_WS_MESSAGE } from "../constants";
|
||||
import { MessageLevel } from "../elements/messages/Message";
|
||||
import { showMessage } from "../elements/messages/MessageContainer";
|
||||
|
||||
export interface WSMessage {
|
||||
message_type: string;
|
||||
}
|
||||
|
||||
export class WebsocketClient {
|
||||
|
||||
messageSocket?: WebSocket;
|
||||
retryDelay = 200;
|
||||
|
||||
constructor() {
|
||||
try {
|
||||
this.connect();
|
||||
} catch (error) {
|
||||
console.warn(`authentik/ws: failed to connect to ws ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (navigator.webdriver) return;
|
||||
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${window.location.host
|
||||
}/ws/client/`;
|
||||
this.messageSocket = new WebSocket(wsUrl);
|
||||
this.messageSocket.addEventListener("open", () => {
|
||||
console.debug(`authentik/ws: connected to ${wsUrl}`);
|
||||
this.retryDelay = 200;
|
||||
});
|
||||
this.messageSocket.addEventListener("close", (e) => {
|
||||
console.debug(`authentik/ws: closed ws connection: ${e}`);
|
||||
if (this.retryDelay > 3000) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`Connection error, reconnecting...`
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
console.debug(`authentik/ws: reconnecting ws in ${this.retryDelay}ms`);
|
||||
this.connect();
|
||||
}, this.retryDelay);
|
||||
this.retryDelay = this.retryDelay * 2;
|
||||
});
|
||||
this.messageSocket.addEventListener("message", (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(EVENT_WS_MESSAGE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: data as WSMessage,
|
||||
})
|
||||
);
|
||||
});
|
||||
this.messageSocket.addEventListener("error", () => {
|
||||
this.retryDelay = this.retryDelay * 2;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@ -3,11 +3,16 @@ 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-rc1";
|
||||
export const VERSION = "2021.7.2";
|
||||
export const PAGE_SIZE = 20;
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
export const EVENT_REFRESH = "ak-refresh";
|
||||
export const EVENT_NOTIFICATION_TOGGLE = "ak-notification-toggle";
|
||||
export const EVENT_SIDEBAR_TOGGLE = "ak-sidebar-toggle";
|
||||
export const EVENT_API_DRAWER_REFRESH = "ak-api-drawer-refresh";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
export const EVENT_WS_MESSAGE = "ak-ws-message";
|
||||
|
||||
export const WS_MSG_TYPE_MESSAGE = "message";
|
||||
export const WS_MSG_TYPE_REFRESH = "refresh";
|
||||
|
||||
@ -4,8 +4,9 @@ import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import AKGlobal from "../authentik.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import { EVENT_SIDEBAR_TOGGLE, TITLE_DEFAULT } from "../constants";
|
||||
import { tenant } from "../api/Config";
|
||||
import { EVENT_NOTIFICATION_TOGGLE, EVENT_SIDEBAR_TOGGLE, TITLE_DEFAULT } from "../constants";
|
||||
import { DEFAULT_CONFIG, tenant } from "../api/Config";
|
||||
import { EventsApi } from "../../api/dist";
|
||||
|
||||
@customElement("ak-page-header")
|
||||
export class PageHeader extends LitElement {
|
||||
@ -16,6 +17,9 @@ export class PageHeader extends LitElement {
|
||||
@property({type: Boolean})
|
||||
iconImage = false
|
||||
|
||||
@property({type: Boolean})
|
||||
hasNotifications = false;
|
||||
|
||||
@property()
|
||||
set header(value: string) {
|
||||
tenant().then(tenant => {
|
||||
@ -44,12 +48,12 @@ export class PageHeader extends LitElement {
|
||||
flex-direction: row;
|
||||
min-height: 114px;
|
||||
}
|
||||
button.pf-c-button.pf-m-plain.sidebar-trigger {
|
||||
.pf-c-button.pf-m-plain {
|
||||
background-color: var(--pf-c-page__main-section--m-light--BackgroundColor);
|
||||
border-radius: 0px;
|
||||
}
|
||||
.pf-c-page__main-section {
|
||||
width: 100%;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@ -57,6 +61,13 @@ export class PageHeader extends LitElement {
|
||||
img.pf-icon {
|
||||
max-height: 24px;
|
||||
}
|
||||
.sidebar-trigger,
|
||||
.notification-trigger {
|
||||
font-size: 24px;
|
||||
}
|
||||
.notification-trigger.has-notifications {
|
||||
color: #2B9AF3;
|
||||
}
|
||||
`];
|
||||
}
|
||||
|
||||
@ -70,6 +81,16 @@ export class PageHeader extends LitElement {
|
||||
return html``;
|
||||
}
|
||||
|
||||
firstUpdated(): void {
|
||||
new EventsApi(DEFAULT_CONFIG).eventsNotificationsList({
|
||||
seen: false,
|
||||
ordering: "-created",
|
||||
pageSize: 1,
|
||||
}).then(r => {
|
||||
this.hasNotifications = r.pagination.count > 0;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<button
|
||||
class="sidebar-trigger pf-c-button pf-m-plain"
|
||||
@ -92,7 +113,19 @@ export class PageHeader extends LitElement {
|
||||
${this.description ?
|
||||
html`<p>${this.description}</p>` : html``}
|
||||
</div>
|
||||
</section>`;
|
||||
</section>
|
||||
<button
|
||||
class="notification-trigger pf-c-button pf-m-plain ${this.hasNotifications ? "has-notifications" : ""}"
|
||||
@click=${() => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}}>
|
||||
<i class="fas fa-bell"></i>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { t } from "@lingui/macro";
|
||||
import { LitElement, html, customElement, TemplateResult, property, CSSResult, css } from "lit-element";
|
||||
import "./Message";
|
||||
import { APIMessage, MessageLevel } from "./Message";
|
||||
import { APIMessage } from "./Message";
|
||||
import PFAlertGroup from "@patternfly/patternfly/components/AlertGroup/alert-group.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import { EVENT_WS_MESSAGE, WS_MSG_TYPE_MESSAGE } from "../../constants";
|
||||
import { WSMessage } from "../../common/ws";
|
||||
|
||||
export function showMessage(message: APIMessage): void {
|
||||
const container = document.querySelector<MessageContainer>("ak-message-container");
|
||||
@ -20,9 +21,6 @@ export class MessageContainer extends LitElement {
|
||||
@property({attribute: false})
|
||||
messages: APIMessage[] = [];
|
||||
|
||||
messageSocket?: WebSocket;
|
||||
retryDelay = 200;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFAlertGroup, css`
|
||||
/* Fix spacing between messages */
|
||||
@ -34,11 +32,10 @@ export class MessageContainer extends LitElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
try {
|
||||
this.connect();
|
||||
} catch (error) {
|
||||
console.warn(`authentik/messages: failed to connect to ws ${error}`);
|
||||
}
|
||||
this.addEventListener(EVENT_WS_MESSAGE, ((e: CustomEvent<WSMessage>) => {
|
||||
if (e.detail.message_type !== WS_MSG_TYPE_MESSAGE) return;
|
||||
this.addMessage(e.detail as unknown as APIMessage);
|
||||
}) as EventListener);
|
||||
}
|
||||
|
||||
// add a new message, but only if the message isn't currently shown.
|
||||
@ -49,40 +46,6 @@ export class MessageContainer extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
connect(): void {
|
||||
if (navigator.webdriver) return;
|
||||
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
|
||||
window.location.host
|
||||
}/ws/client/`;
|
||||
this.messageSocket = new WebSocket(wsUrl);
|
||||
this.messageSocket.addEventListener("open", () => {
|
||||
console.debug(`authentik/messages: connected to ${wsUrl}`);
|
||||
this.retryDelay = 200;
|
||||
});
|
||||
this.messageSocket.addEventListener("close", (e) => {
|
||||
console.debug(`authentik/messages: closed ws connection: ${e}`);
|
||||
if (this.retryDelay > 3000) {
|
||||
showMessage({
|
||||
level: MessageLevel.error,
|
||||
message: t`Connection error, reconnecting...`
|
||||
});
|
||||
}
|
||||
setTimeout(() => {
|
||||
console.debug(`authentik/messages: reconnecting ws in ${this.retryDelay}ms`);
|
||||
this.connect();
|
||||
}, this.retryDelay);
|
||||
this.retryDelay = this.retryDelay * 2;
|
||||
});
|
||||
this.messageSocket.addEventListener("message", (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
this.addMessage(data);
|
||||
this.requestUpdate();
|
||||
});
|
||||
this.messageSocket.addEventListener("error", () => {
|
||||
this.retryDelay = this.retryDelay * 2;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ul class="pf-c-alert-group pf-m-toast">
|
||||
${this.messages.map((m) => {
|
||||
|
||||
@ -10,6 +10,7 @@ import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import AKGlobal from "../../authentik.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import { EVENT_NOTIFICATION_TOGGLE } from "../../constants";
|
||||
import { ActionToLabel } from "../../pages/events/utils";
|
||||
|
||||
@customElement("ak-notification-drawer")
|
||||
export class NotificationDrawer extends LitElement {
|
||||
@ -23,6 +24,15 @@ export class NotificationDrawer extends LitElement {
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFButton, PFNotificationDrawer, PFContent, PFDropdown, AKGlobal].concat(
|
||||
css`
|
||||
.pf-c-notification-drawer__header {
|
||||
height: 114px;
|
||||
align-items: center;
|
||||
}
|
||||
.pf-c-notification-drawer__header-action,
|
||||
.pf-c-notification-drawer__header-action-close,
|
||||
.pf-c-notification-drawer__header-action-close > .pf-c-button.pf-m-plain {
|
||||
height: 100%;
|
||||
}
|
||||
.pf-c-notification-drawer__list-item-description {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
@ -55,13 +65,13 @@ export class NotificationDrawer extends LitElement {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return html`<li class="pf-c-notification-drawer__list-item pf-m-read">
|
||||
return html`<li class="pf-c-notification-drawer__list-item">
|
||||
<div class="pf-c-notification-drawer__list-item-header">
|
||||
<span class="pf-c-notification-drawer__list-item-header-icon ${level}">
|
||||
<i class="fas fa-info-circle" aria-hidden="true"></i>
|
||||
</span>
|
||||
<h2 class="pf-c-notification-drawer__list-item-header-title">
|
||||
${item.event?.action}
|
||||
${ActionToLabel(item.event?.action)}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="pf-c-notification-drawer__list-item-action">
|
||||
@ -95,12 +105,14 @@ export class NotificationDrawer extends LitElement {
|
||||
return html`<div class="pf-c-drawer__body pf-m-no-padding">
|
||||
<div class="pf-c-notification-drawer">
|
||||
<div class="pf-c-notification-drawer__header">
|
||||
<h1 class="pf-c-notification-drawer__header-title">
|
||||
${t`Notifications`}
|
||||
</h1>
|
||||
<span class="pf-c-notification-drawer__header-status">
|
||||
${t`${this.unread} unread`}
|
||||
</span>
|
||||
<div class="text">
|
||||
<h1 class="pf-c-notification-drawer__header-title">
|
||||
${t`Notifications`}
|
||||
</h1>
|
||||
<span>
|
||||
${t`${this.unread} unread`}
|
||||
</span>
|
||||
</div>
|
||||
<div class="pf-c-notification-drawer__header-action">
|
||||
<div class="pf-c-notification-drawer__header-action-close">
|
||||
<button
|
||||
|
||||
@ -1,31 +0,0 @@
|
||||
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFDropdown from "@patternfly/patternfly/components/Dropdown/dropdown.css";
|
||||
import FA from "@fortawesome/fontawesome-free/css/fontawesome.css";
|
||||
import { EVENT_NOTIFICATION_TOGGLE } from "../../constants";
|
||||
|
||||
@customElement("ak-notification-trigger")
|
||||
export class NotificationRule extends LitElement {
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFDropdown, FA];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener("click", () => {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent(EVENT_NOTIFICATION_TOGGLE, {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
// TODO: Show icon with red dot when unread notifications exist
|
||||
return html`<i class="fas fa-bell pf-c-dropdown__toggle-icon" aria-hidden="true"></i>`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -5,7 +5,6 @@ import { me } from "../../api/Users";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import "../notifications/NotificationTrigger";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
|
||||
@customElement("ak-sidebar-user")
|
||||
@ -39,8 +38,6 @@ export class SidebarUser extends LitElement {
|
||||
return html`<img class="pf-c-avatar" src="${ifDefined(u.user.avatar)}" alt="" />`;
|
||||
}), html``)}
|
||||
</a>
|
||||
<ak-notification-trigger class="pf-c-nav__link user-notifications">
|
||||
</ak-notification-trigger>
|
||||
<a href="/flows/-/default/invalidation/" class="pf-c-nav__link user-logout" id="logout">
|
||||
<i class="fas fa-sign-out-alt" aria-hidden="true"></i>
|
||||
</a>
|
||||
|
||||
@ -33,6 +33,7 @@ import { until } from "lit-html/directives/until";
|
||||
import { PFSize } from "../elements/Spinner";
|
||||
import { TITLE_DEFAULT } from "../constants";
|
||||
import { configureSentry } from "../api/Sentry";
|
||||
import { WebsocketClient } from "../common/ws";
|
||||
|
||||
@customElement("ak-flow-executor")
|
||||
export class FlowExecutor extends LitElement implements StageHost {
|
||||
@ -48,6 +49,8 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
@property({ attribute: false })
|
||||
tenant?: CurrentTenant;
|
||||
|
||||
ws: WebsocketClient;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFLogin, PFButton, PFTitle, PFList, PFBackgroundImage, AKGlobal].concat(css`
|
||||
.ak-loading {
|
||||
@ -75,6 +78,8 @@ export class FlowExecutor extends LitElement implements StageHost {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
this.ws.connect();
|
||||
this.flowSlug = window.location.pathname.split("/")[3];
|
||||
}
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFDrawer from "@patternfly/patternfly/components/Drawer/drawer.css";
|
||||
import AKGlobal from "../authentik.css";
|
||||
|
||||
import "../elements/router/RouterOutlet";
|
||||
import "../elements/messages/MessageContainer";
|
||||
@ -18,6 +19,7 @@ import { until } from "lit-html/directives/until";
|
||||
import { EVENT_NOTIFICATION_TOGGLE, EVENT_SIDEBAR_TOGGLE, VERSION } from "../constants";
|
||||
import { AdminApi } from "authentik-api";
|
||||
import { DEFAULT_CONFIG } from "../api/Config";
|
||||
import { WebsocketClient } from "../common/ws";
|
||||
|
||||
|
||||
@customElement("ak-interface-admin")
|
||||
@ -29,8 +31,10 @@ export class AdminInterface extends LitElement {
|
||||
@property({type: Boolean})
|
||||
notificationOpen = false;
|
||||
|
||||
ws: WebsocketClient;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [PFBase, PFPage, PFButton, PFDrawer, css`
|
||||
return [PFBase, PFPage, PFButton, PFDrawer, AKGlobal, css`
|
||||
.pf-c-page__main, .pf-c-drawer__content, .pf-c-page__drawer {
|
||||
z-index: auto !important;
|
||||
}
|
||||
@ -39,6 +43,8 @@ export class AdminInterface extends LitElement {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.ws = new WebsocketClient();
|
||||
this.ws.connect();
|
||||
this.sidebarOpen = window.innerWidth >= 1280;
|
||||
window.addEventListener("resize", () => {
|
||||
this.sidebarOpen = window.innerWidth >= 1280;
|
||||
|
||||
@ -208,6 +208,10 @@ msgstr "Application"
|
||||
msgid "Application Icon"
|
||||
msgstr "Application Icon"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Application authorized"
|
||||
msgstr "Application authorized"
|
||||
|
||||
#: src/flows/stages/consent/ConsentStage.ts
|
||||
msgid "Application requires following permissions:"
|
||||
msgstr "Application requires following permissions:"
|
||||
@ -676,6 +680,10 @@ msgstr "Confidential clients are capable of maintaining the confidentiality of t
|
||||
msgid "Configuration"
|
||||
msgstr "Configuration"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Configuration error"
|
||||
msgstr "Configuration error"
|
||||
|
||||
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
|
||||
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
@ -735,7 +743,7 @@ msgstr "Connect"
|
||||
msgid "Connected."
|
||||
msgstr "Connected."
|
||||
|
||||
#: src/elements/messages/MessageContainer.ts
|
||||
#: src/common/ws.ts
|
||||
msgid "Connection error, reconnecting..."
|
||||
msgstr "Connection error, reconnecting..."
|
||||
|
||||
@ -1222,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."
|
||||
@ -1299,6 +1311,10 @@ msgstr "Email address"
|
||||
msgid "Email info:"
|
||||
msgstr "Email info:"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Email sent"
|
||||
msgstr "Email sent"
|
||||
|
||||
#: src/pages/stages/prompt/PromptForm.ts
|
||||
msgid "Email: Text field with Email type."
|
||||
msgstr "Email: Text field with Email type."
|
||||
@ -1396,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}"
|
||||
@ -1404,6 +1424,10 @@ 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
|
||||
@ -1505,6 +1529,10 @@ msgstr "External host"
|
||||
msgid "Failed attempts before cancel"
|
||||
msgstr "Failed attempts before cancel"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Failed login"
|
||||
msgstr "Failed login"
|
||||
|
||||
#: src/pages/admin-overview/charts/LDAPSyncStatusChart.ts
|
||||
msgid "Failed sources"
|
||||
msgstr "Failed sources"
|
||||
@ -1650,6 +1678,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)"
|
||||
@ -1674,6 +1706,10 @@ msgstr "From address"
|
||||
msgid "GID start number"
|
||||
msgstr "GID start number"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "General system exception"
|
||||
msgstr "General system exception"
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "General system status"
|
||||
msgstr "General system status"
|
||||
@ -1741,6 +1777,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"
|
||||
@ -1857,6 +1897,14 @@ msgstr "If your authentik Instance is using a self-signed certificate, set this
|
||||
msgid "Impersonate"
|
||||
msgstr "Impersonate"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Impersonation ended"
|
||||
msgstr "Impersonation ended"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Impersonation started"
|
||||
msgstr "Impersonation started"
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Import"
|
||||
@ -1918,6 +1966,10 @@ msgstr "Invalidation flow"
|
||||
msgid "Invitation"
|
||||
msgstr "Invitation"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Invitation used"
|
||||
msgstr "Invitation used"
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
#: src/pages/stages/invitation/InvitationListPage.ts
|
||||
msgid "Invitations"
|
||||
@ -2028,6 +2080,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"
|
||||
@ -2116,6 +2172,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
|
||||
@ -2136,6 +2193,10 @@ msgstr "Local"
|
||||
msgid "Log the currently pending user in."
|
||||
msgstr "Log the currently pending user in."
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Login"
|
||||
msgstr "Login"
|
||||
|
||||
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
||||
msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP."
|
||||
msgstr "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP."
|
||||
@ -2157,6 +2218,10 @@ msgstr "Logins over the last 24 hours"
|
||||
msgid "Logo"
|
||||
msgstr "Logo"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Logout"
|
||||
msgstr "Logout"
|
||||
|
||||
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts
|
||||
msgid "Logout URL"
|
||||
msgstr "Logout URL"
|
||||
@ -2247,6 +2312,18 @@ msgstr "Mode"
|
||||
msgid "Model Name"
|
||||
msgstr "Model Name"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Model created"
|
||||
msgstr "Model created"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Model deleted"
|
||||
msgstr "Model deleted"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Model updated"
|
||||
msgstr "Model updated"
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
msgid "Monitor"
|
||||
msgstr "Monitor"
|
||||
@ -2591,6 +2668,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"
|
||||
@ -2665,6 +2746,10 @@ msgstr "Password"
|
||||
msgid "Password field"
|
||||
msgstr "Password field"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Password set"
|
||||
msgstr "Password set"
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "Password stage"
|
||||
msgstr "Password stage"
|
||||
@ -2730,6 +2815,14 @@ msgstr "Policy binding"
|
||||
msgid "Policy engine mode"
|
||||
msgstr "Policy engine mode"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Policy exception"
|
||||
msgstr "Policy exception"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Policy execution"
|
||||
msgstr "Policy execution"
|
||||
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
msgid "Policy {0}"
|
||||
msgstr "Policy {0}"
|
||||
@ -2798,6 +2891,10 @@ msgstr "Prompts"
|
||||
msgid "Property Mapping"
|
||||
msgstr "Property Mapping"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Property Mapping exception"
|
||||
msgstr "Property Mapping exception"
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
#: src/pages/property-mappings/PropertyMappingListPage.ts
|
||||
msgid "Property Mappings"
|
||||
@ -2918,6 +3015,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"
|
||||
@ -3144,6 +3245,14 @@ msgstr "Search..."
|
||||
msgid "Secret key"
|
||||
msgstr "Secret key"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Secret was rotation"
|
||||
msgstr "Secret was rotation"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Secret was viewed"
|
||||
msgstr "Secret was viewed"
|
||||
|
||||
#: src/pages/events/EventInfo.ts
|
||||
msgid "Secret:"
|
||||
msgstr "Secret:"
|
||||
@ -3163,6 +3272,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."
|
||||
@ -3228,6 +3341,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."
|
||||
@ -3349,6 +3466,10 @@ msgstr "Something went wrong! Please try again later."
|
||||
msgid "Source"
|
||||
msgstr "Source"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Source linked"
|
||||
msgstr "Source linked"
|
||||
|
||||
#: src/pages/user-settings/settings/SourceSettingsOAuth.ts
|
||||
msgid "Source {0}"
|
||||
msgstr "Source {0}"
|
||||
@ -3768,6 +3889,10 @@ msgstr "Superuser-groups"
|
||||
msgid "Superusers"
|
||||
msgstr "Superusers"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Suspicious request"
|
||||
msgstr "Suspicious request"
|
||||
|
||||
#: src/pages/policies/password/PasswordPolicyForm.ts
|
||||
msgid "Symbol charset"
|
||||
msgstr "Symbol charset"
|
||||
@ -3797,6 +3922,18 @@ 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"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "System task execution"
|
||||
msgstr "System task execution"
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "TLS Authentication Certificate"
|
||||
msgstr "TLS Authentication Certificate"
|
||||
@ -3933,6 +4070,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."
|
||||
@ -4255,6 +4396,10 @@ msgstr "Update Token"
|
||||
msgid "Update User"
|
||||
msgstr "Update User"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Update available"
|
||||
msgstr "Update available"
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
msgid "Update details"
|
||||
msgstr "Update details"
|
||||
@ -4375,6 +4520,10 @@ msgstr "User object filter"
|
||||
msgid "User password writeback"
|
||||
msgstr "User password writeback"
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "User was written to"
|
||||
msgstr "User was written to"
|
||||
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
msgid "User {0}"
|
||||
@ -4492,6 +4641,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"
|
||||
@ -4559,6 +4710,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!"
|
||||
|
||||
@ -208,6 +208,10 @@ msgstr ""
|
||||
msgid "Application Icon"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Application authorized"
|
||||
msgstr ""
|
||||
|
||||
#: src/flows/stages/consent/ConsentStage.ts
|
||||
msgid "Application requires following permissions:"
|
||||
msgstr ""
|
||||
@ -670,6 +674,10 @@ msgstr ""
|
||||
msgid "Configuration"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Configuration error"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/authenticator_duo/AuthenticatorDuoStageForm.ts
|
||||
#: src/pages/stages/authenticator_static/AuthenticatorStaticStageForm.ts
|
||||
#: src/pages/stages/authenticator_totp/AuthenticatorTOTPStageForm.ts
|
||||
@ -729,7 +737,7 @@ msgstr ""
|
||||
msgid "Connected."
|
||||
msgstr ""
|
||||
|
||||
#: src/elements/messages/MessageContainer.ts
|
||||
#: src/common/ws.ts
|
||||
msgid "Connection error, reconnecting..."
|
||||
msgstr ""
|
||||
|
||||
@ -1214,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 ""
|
||||
@ -1291,6 +1303,10 @@ msgstr ""
|
||||
msgid "Email info:"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Email sent"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/prompt/PromptForm.ts
|
||||
msgid "Email: Text field with Email type."
|
||||
msgstr ""
|
||||
@ -1388,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 ""
|
||||
@ -1396,6 +1416,10 @@ 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
|
||||
@ -1497,6 +1521,10 @@ msgstr ""
|
||||
msgid "Failed attempts before cancel"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Failed login"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/charts/LDAPSyncStatusChart.ts
|
||||
msgid "Failed sources"
|
||||
msgstr ""
|
||||
@ -1642,6 +1670,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 ""
|
||||
@ -1666,6 +1698,10 @@ msgstr ""
|
||||
msgid "GID start number"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "General system exception"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/admin-overview/AdminOverviewPage.ts
|
||||
msgid "General system status"
|
||||
msgstr ""
|
||||
@ -1733,6 +1769,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 ""
|
||||
@ -1849,6 +1889,14 @@ msgstr ""
|
||||
msgid "Impersonate"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Impersonation ended"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Impersonation started"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
#: src/pages/flows/FlowListPage.ts
|
||||
msgid "Import"
|
||||
@ -1910,6 +1958,10 @@ msgstr ""
|
||||
msgid "Invitation"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Invitation used"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
#: src/pages/stages/invitation/InvitationListPage.ts
|
||||
msgid "Invitations"
|
||||
@ -2020,6 +2072,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"
|
||||
@ -2108,6 +2164,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
|
||||
@ -2128,6 +2185,10 @@ msgstr ""
|
||||
msgid "Log the currently pending user in."
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Login"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/sources/ldap/LDAPSourceForm.ts
|
||||
msgid "Login password is synced from LDAP into authentik automatically. Enable this option only to write password changes in authentik back to LDAP."
|
||||
msgstr ""
|
||||
@ -2149,6 +2210,10 @@ msgstr ""
|
||||
msgid "Logo"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Logout"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/providers/oauth2/OAuth2ProviderViewPage.ts
|
||||
msgid "Logout URL"
|
||||
msgstr ""
|
||||
@ -2239,6 +2304,18 @@ msgstr ""
|
||||
msgid "Model Name"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Model created"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Model deleted"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Model updated"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
msgid "Monitor"
|
||||
msgstr ""
|
||||
@ -2583,6 +2660,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 ""
|
||||
@ -2657,6 +2738,10 @@ msgstr ""
|
||||
msgid "Password field"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Password set"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||
msgid "Password stage"
|
||||
msgstr ""
|
||||
@ -2722,6 +2807,14 @@ msgstr ""
|
||||
msgid "Policy engine mode"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Policy exception"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Policy execution"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
msgid "Policy {0}"
|
||||
msgstr ""
|
||||
@ -2790,6 +2883,10 @@ msgstr ""
|
||||
msgid "Property Mapping"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Property Mapping exception"
|
||||
msgstr ""
|
||||
|
||||
#: src/interfaces/AdminInterface.ts
|
||||
#: src/pages/property-mappings/PropertyMappingListPage.ts
|
||||
msgid "Property Mappings"
|
||||
@ -2910,6 +3007,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 ""
|
||||
@ -3136,6 +3237,14 @@ msgstr ""
|
||||
msgid "Secret key"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Secret was rotation"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Secret was viewed"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/EventInfo.ts
|
||||
msgid "Secret:"
|
||||
msgstr ""
|
||||
@ -3155,6 +3264,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 ""
|
||||
@ -3220,6 +3333,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 ""
|
||||
@ -3341,6 +3458,10 @@ msgstr ""
|
||||
msgid "Source"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Source linked"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/settings/SourceSettingsOAuth.ts
|
||||
msgid "Source {0}"
|
||||
msgstr ""
|
||||
@ -3760,6 +3881,10 @@ msgstr ""
|
||||
msgid "Superusers"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Suspicious request"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/policies/password/PasswordPolicyForm.ts
|
||||
msgid "Symbol charset"
|
||||
msgstr ""
|
||||
@ -3789,6 +3914,18 @@ 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 ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "System task execution"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/outposts/ServiceConnectionDockerForm.ts
|
||||
msgid "TLS Authentication Certificate"
|
||||
msgstr ""
|
||||
@ -3918,6 +4055,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 ""
|
||||
@ -4240,6 +4381,10 @@ msgstr ""
|
||||
msgid "Update User"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "Update available"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/user-settings/UserDetailsPage.ts
|
||||
msgid "Update details"
|
||||
msgstr ""
|
||||
@ -4360,6 +4505,10 @@ msgstr ""
|
||||
msgid "User password writeback"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/events/utils.ts
|
||||
msgid "User was written to"
|
||||
msgstr ""
|
||||
|
||||
#: src/pages/policies/BoundPoliciesList.ts
|
||||
#: src/pages/users/UserViewPage.ts
|
||||
msgid "User {0}"
|
||||
@ -4477,6 +4626,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"
|
||||
@ -4544,6 +4695,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}`;
|
||||
}
|
||||
|
||||
}
|
||||
@ -42,7 +42,9 @@ export class PolicyStatusChart extends AKChart<PolicyMetrics> {
|
||||
})).pagination.count;
|
||||
this.centerText = count.toString();
|
||||
return {
|
||||
count: count - cached - unbound,
|
||||
// If we have more cache than total policies, only show that
|
||||
// otherwise show count without unbound
|
||||
count: cached >= count ? cached : count - unbound,
|
||||
cached: cached,
|
||||
unbound: unbound,
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
@ -8,6 +8,7 @@ import { PAGE_SIZE } from "../../constants";
|
||||
import { TableColumn } from "../../elements/table/Table";
|
||||
import { TablePage } from "../../elements/table/TablePage";
|
||||
import "./EventInfo";
|
||||
import { ActionToLabel } from "./utils";
|
||||
|
||||
@customElement("ak-event-list")
|
||||
export class EventListPage extends TablePage<Event> {
|
||||
@ -51,7 +52,7 @@ export class EventListPage extends TablePage<Event> {
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
return [
|
||||
html`<div>${item.action}</div>
|
||||
html`<div>${ActionToLabel(item.action)}</div>
|
||||
<small>${item.app}</small>`,
|
||||
item.user?.username ?
|
||||
html`<a href="#/identity/users/${item.user.pk}">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user