Compare commits
55 Commits
version/20
...
version-20
Author | SHA1 | Date | |
---|---|---|---|
def0a42bf1 | |||
727e55e44b | |||
cd88b91686 | |||
8eb73d3a16 | |||
83f46f6ff1 | |||
0e7cc6da4c | |||
a262171671 | |||
87b8ca7be4 | |||
cc8dc1403f | |||
f21a196a3b | |||
f3a72761c0 | |||
77a67dcbc1 | |||
8d7ce49101 | |||
841c13ed77 | |||
30d708dd1f | |||
8a50279142 | |||
f1e1911788 | |||
0b712d22a8 | |||
9d0a7578ec | |||
f8fab14e1e | |||
9b6e07de17 | |||
4e2ba8c916 | |||
6b35d0c70b | |||
dd65862bf2 | |||
2206b71f6f | |||
24e02c82dc | |||
2b6213c3ce | |||
d51d14fd32 | |||
35679f5abb | |||
98666cc5e9 | |||
dbaad90c3e | |||
63b5656cca | |||
96713a82dd | |||
2b20b89c80 | |||
cbb24dfddd | |||
056ff5ff59 | |||
4da2f44f8e | |||
3da7fcfc1d | |||
6ea57921f2 | |||
c7ea4b5a7f | |||
c2933f0681 | |||
27636cc49f | |||
42196f554e | |||
ad5fc139eb | |||
3a68de0d38 | |||
93984b35b3 | |||
d25d547486 | |||
b84bc418af | |||
ea94750ea8 | |||
a3aa7a8d4f | |||
7004cb1c91 | |||
e67464b8a0 | |||
b0d4f035f1 | |||
661d2ec701 | |||
3f570bb96d |
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 2022.10.0
|
||||
current_version = 2022.10.4
|
||||
tag = True
|
||||
commit = True
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)
|
||||
|
@ -17,24 +17,24 @@ diverse, inclusive, and healthy community.
|
||||
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
|
||||
- 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
|
||||
- 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
|
||||
|
||||
@ -106,7 +106,7 @@ 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
|
||||
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
|
||||
|
@ -11,19 +11,22 @@ The following is a set of guidelines for contributing to authentik and its compo
|
||||
[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)
|
||||
* [The components](#the-components)
|
||||
* [authentik's structure](#authentiks-structure)
|
||||
|
||||
- [The components](#the-components)
|
||||
- [authentik's structure](#authentiks-structure)
|
||||
|
||||
[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)
|
||||
|
||||
- [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)
|
||||
* [Python Styleguide](#python-styleguide)
|
||||
* [Documentation Styleguide](#documentation-styleguide)
|
||||
|
||||
- [Git Commit Messages](#git-commit-messages)
|
||||
- [Python Styleguide](#python-styleguide)
|
||||
- [Documentation Styleguide](#documentation-styleguide)
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
@ -39,11 +42,11 @@ Either [create a question on GitHub](https://github.com/goauthentik/authentik/is
|
||||
|
||||
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_ 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
|
||||
|
||||
@ -137,10 +140,10 @@ This is documented in the [developer docs](https://goauthentik.io/developer-docs
|
||||
|
||||
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
|
||||
- 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:
|
||||
|
||||
@ -154,10 +157,10 @@ While the prerequisites above must be satisfied prior to having your pull reques
|
||||
|
||||
### 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
|
||||
- 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
|
||||
|
||||
@ -165,11 +168,11 @@ All Python code is linted with [black](https://black.readthedocs.io/en/stable/),
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- Use [MDX](https://mdxjs.com/) whenever appropriate.
|
||||
|
@ -3,6 +3,7 @@ FROM --platform=${BUILDPLATFORM} docker.io/node:18 as website-builder
|
||||
|
||||
COPY ./website /work/website/
|
||||
COPY ./blueprints /work/blueprints/
|
||||
COPY ./SECURITY.md /work/
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /work/website
|
||||
|
@ -26,10 +26,10 @@ For bigger setups, there is a Helm Chart [here](https://github.com/goauthentik/h
|
||||
|
||||
## Screenshots
|
||||
|
||||
Light | Dark
|
||||
--- | ---
|
||||
 | 
|
||||
 | 
|
||||
| Light | Dark |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------- |
|
||||
|  |  |
|
||||
|  |  |
|
||||
|
||||
## Development
|
||||
|
||||
|
38
SECURITY.md
38
SECURITY.md
@ -1,17 +1,43 @@
|
||||
# Security Policy
|
||||
Authentik takes security very seriously. We follow the rules of [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure), and we urge our community to do so as well, instead of reporting vulnerabilities publicly. This allows us to patch the issue quickly, announce it's existence and release the fixed version.
|
||||
|
||||
## Supported Versions
|
||||
|
||||
(.x being the latest patch release for each version)
|
||||
|
||||
| Version | Supported |
|
||||
| ---------- | ------------------ |
|
||||
| 2022.9.x | :white_check_mark: |
|
||||
| 2022.10.x | :white_check_mark: |
|
||||
| Version | Supported |
|
||||
| --------- | ------------------ |
|
||||
| 2022.10.x | :white_check_mark: |
|
||||
| 2022.11.x | :white_check_mark: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io)
|
||||
To report a vulnerability, send an email to [security@goauthentik.io](mailto:security@goauthentik.io). Be sure to include relevant information like which version you've found the issue in, instructions on how to reproduce the issue, and anything else that might make it easier for us to find the bug.
|
||||
|
||||
## Criticality levels
|
||||
|
||||
### High
|
||||
|
||||
- Authorization bypass
|
||||
- Circumvention of policies
|
||||
|
||||
### Moderate
|
||||
|
||||
- Denial-of-Service attacks
|
||||
|
||||
### Low
|
||||
|
||||
- Unvalidated redirects
|
||||
- Issues requiring uncommon setups
|
||||
|
||||
## Disclosure process
|
||||
|
||||
1. Issue is reported via Email as listed above.
|
||||
2. The authentik Security team will try to reproduce the issue and ask for more information if required.
|
||||
3. A criticality level is assigned.
|
||||
4. A fix is created, and if possible tested by the issue reporter.
|
||||
5. The fix is backported to other supported versions, and if possible a workaround for other versions is created.
|
||||
6. An announcement is sent out with a fixed release date and criticality level of the issue. The announcement will be sent at least 24 hours before the release of the fix
|
||||
7. The fixed version is released for the supported versions.
|
||||
|
||||
## Getting security notifications
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
from os import environ
|
||||
from typing import Optional
|
||||
|
||||
__version__ = "2022.10.0"
|
||||
__version__ = "2022.10.4"
|
||||
ENV_GIT_HASH_KEY = "GIT_BUILD_HASH"
|
||||
|
||||
|
||||
|
@ -63,7 +63,7 @@ class BlueprintEntry:
|
||||
all_attrs = get_attrs(model)
|
||||
|
||||
for extra_identifier_name in extra_identifier_names:
|
||||
identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name)
|
||||
identifiers[extra_identifier_name] = all_attrs.pop(extra_identifier_name, None)
|
||||
return BlueprintEntry(
|
||||
identifiers=identifiers,
|
||||
model=f"{model._meta.app_label}.{model._meta.model_name}",
|
||||
@ -139,7 +139,7 @@ class KeyOf(YAMLTag):
|
||||
):
|
||||
return _entry._state.instance.pbm_uuid
|
||||
return _entry._state.instance.pk
|
||||
raise ValueError(
|
||||
raise EntryInvalidError(
|
||||
f"KeyOf: failed to find entry with `id` of `{self.id_from}` and a model instance"
|
||||
)
|
||||
|
||||
|
@ -470,7 +470,7 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
# pylint: disable=invalid-name, unused-argument
|
||||
def recovery_email(self, request: Request, pk: int) -> Response:
|
||||
"""Create a temporary link that a user can use to recover their accounts"""
|
||||
for_user = self.get_object()
|
||||
for_user: User = self.get_object()
|
||||
if for_user.email == "":
|
||||
LOGGER.debug("User doesn't have an email address")
|
||||
return Response(status=404)
|
||||
@ -488,8 +488,9 @@ class UserViewSet(UsedByMixin, ModelViewSet):
|
||||
email_stage: EmailStage = stages.first()
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(email_stage.subject),
|
||||
template_name=email_stage.template,
|
||||
to=[for_user.email],
|
||||
template_name=email_stage.template,
|
||||
language=for_user.locale(request),
|
||||
template_context={
|
||||
"url": link,
|
||||
"user": for_user,
|
||||
|
@ -4,6 +4,7 @@ from typing import Callable, Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import activate
|
||||
from sentry_sdk.api import set_tag
|
||||
from structlog.contextvars import STRUCTLOG_KEY_PREFIX
|
||||
|
||||
@ -29,6 +30,10 @@ class ImpersonateMiddleware:
|
||||
def __call__(self, request: HttpRequest) -> HttpResponse:
|
||||
# No permission checks are done here, they need to be checked before
|
||||
# SESSION_KEY_IMPERSONATE_USER is set.
|
||||
if request.user.is_authenticated:
|
||||
locale = request.user.locale(request)
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
|
||||
if SESSION_KEY_IMPERSONATE_USER in request.session:
|
||||
request.user = request.session[SESSION_KEY_IMPERSONATE_USER]
|
||||
|
@ -220,6 +220,17 @@ class User(SerializerModel, GuardianUserMixin, AbstractUser):
|
||||
"""Generate a globally unique UID, based on the user ID and the hashed secret key"""
|
||||
return sha256(f"{self.id}-{settings.SECRET_KEY}".encode("ascii")).hexdigest()
|
||||
|
||||
def locale(self, request: Optional[HttpRequest] = None) -> str:
|
||||
"""Get the locale the user has configured"""
|
||||
try:
|
||||
return self.attributes.get("settings", {}).get("locale", "")
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exc:
|
||||
LOGGER.warning("Failed to get default locale", exc=exc)
|
||||
if request:
|
||||
return request.tenant.locale
|
||||
return ""
|
||||
|
||||
@property
|
||||
def avatar(self) -> str:
|
||||
"""Get avatar, depending on authentik.avatar setting"""
|
||||
|
@ -5,7 +5,7 @@ from typing import Any, Optional
|
||||
from django.contrib import messages
|
||||
from django.db import IntegrityError
|
||||
from django.db.models.query_utils import Q
|
||||
from django.http import HttpRequest, HttpResponse, HttpResponseBadRequest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext as _
|
||||
@ -23,8 +23,10 @@ from authentik.flows.planner import (
|
||||
PLAN_CONTEXT_SSO,
|
||||
FlowPlanner,
|
||||
)
|
||||
from authentik.flows.stage import StageView
|
||||
from authentik.flows.views.executor import NEXT_ARG_NAME, SESSION_KEY_GET, SESSION_KEY_PLAN
|
||||
from authentik.lib.utils.urls import redirect_with_qs
|
||||
from authentik.lib.views import bad_request_message
|
||||
from authentik.policies.denied import AccessDeniedResponse
|
||||
from authentik.policies.utils import delete_none_keys
|
||||
from authentik.stages.password import BACKEND_INBUILT
|
||||
@ -43,6 +45,26 @@ class Action(Enum):
|
||||
DENY = "deny"
|
||||
|
||||
|
||||
class MessageStage(StageView):
|
||||
"""Show a pre-configured message after the flow is done"""
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""Show a pre-configured message after the flow is done"""
|
||||
message = getattr(self.executor.current_stage, "message", "")
|
||||
level = getattr(self.executor.current_stage, "level", messages.SUCCESS)
|
||||
messages.add_message(
|
||||
self.request,
|
||||
level,
|
||||
message,
|
||||
)
|
||||
return self.executor.stage_ok()
|
||||
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
|
||||
|
||||
class SourceFlowManager:
|
||||
"""Help sources decide what they should do after authorization. Based on source settings and
|
||||
previous connections, authenticate the user, enroll a new user, link to an existing user
|
||||
@ -150,16 +172,16 @@ class SourceFlowManager:
|
||||
action, connection = self.get_action(**kwargs)
|
||||
except IntegrityError as exc:
|
||||
self._logger.warning("failed to get action", exc=exc)
|
||||
return redirect("/")
|
||||
return redirect(reverse("authentik_core:root-redirect"))
|
||||
self._logger.debug("get_action", action=action, connection=connection)
|
||||
try:
|
||||
if connection:
|
||||
if action == Action.LINK:
|
||||
self._logger.debug("Linking existing user")
|
||||
return self.handle_existing_user_link(connection)
|
||||
return self.handle_existing_link(connection)
|
||||
if action == Action.AUTH:
|
||||
self._logger.debug("Handling auth user")
|
||||
return self.handle_auth_user(connection)
|
||||
return self.handle_auth(connection)
|
||||
if action == Action.ENROLL:
|
||||
self._logger.debug("Handling enrollment of new user")
|
||||
return self.handle_enroll(connection)
|
||||
@ -198,8 +220,12 @@ class SourceFlowManager:
|
||||
]
|
||||
return []
|
||||
|
||||
def _handle_login_flow(
|
||||
self, flow: Flow, connection: UserSourceConnection, **kwargs
|
||||
def _prepare_flow(
|
||||
self,
|
||||
flow: Flow,
|
||||
connection: UserSourceConnection,
|
||||
stages: Optional[list[StageView]] = None,
|
||||
**kwargs,
|
||||
) -> HttpResponse:
|
||||
"""Prepare Authentication Plan, redirect user FlowExecutor"""
|
||||
# Ensure redirect is carried through when user was trying to
|
||||
@ -219,12 +245,18 @@ class SourceFlowManager:
|
||||
)
|
||||
kwargs.update(self.policy_context)
|
||||
if not flow:
|
||||
return HttpResponseBadRequest()
|
||||
return bad_request_message(
|
||||
self.request,
|
||||
_("Configured flow does not exist."),
|
||||
)
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
planner = FlowPlanner(flow)
|
||||
plan = planner.plan(self.request, kwargs)
|
||||
for stage in self.get_stages_to_append(flow):
|
||||
plan.append_stage(stage=stage)
|
||||
plan.append_stage(stage)
|
||||
if stages:
|
||||
for stage in stages:
|
||||
plan.append_stage(stage)
|
||||
self.request.session[SESSION_KEY_PLAN] = plan
|
||||
return redirect_with_qs(
|
||||
"authentik_core:if-flow",
|
||||
@ -233,24 +265,35 @@ class SourceFlowManager:
|
||||
)
|
||||
|
||||
# pylint: disable=unused-argument
|
||||
def handle_auth_user(
|
||||
def handle_auth(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Login user and redirect."""
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully authenticated with %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
flow_kwargs = {PLAN_CONTEXT_PENDING_USER: connection.user}
|
||||
return self._handle_login_flow(self.source.authentication_flow, connection, **flow_kwargs)
|
||||
return self._prepare_flow(
|
||||
self.source.authentication_flow,
|
||||
connection,
|
||||
stages=[
|
||||
in_memory_stage(
|
||||
MessageStage,
|
||||
message=_(
|
||||
"Successfully authenticated with %(source)s!" % {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
],
|
||||
**flow_kwargs,
|
||||
)
|
||||
|
||||
def handle_existing_user_link(
|
||||
def handle_existing_link(
|
||||
self,
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""Handler when the user was already authenticated and linked an external source
|
||||
to their account."""
|
||||
# When request isn't authenticated we jump straight to auth
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_auth(connection)
|
||||
# Connection has already been saved
|
||||
Event.new(
|
||||
EventAction.SOURCE_LINKED,
|
||||
@ -261,9 +304,6 @@ class SourceFlowManager:
|
||||
self.request,
|
||||
_("Successfully linked %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
# When request isn't authenticated we jump straight to auth
|
||||
if not self.request.user.is_authenticated:
|
||||
return self.handle_auth_user(connection)
|
||||
return redirect(
|
||||
reverse(
|
||||
"authentik_core:if-user",
|
||||
@ -276,18 +316,24 @@ class SourceFlowManager:
|
||||
connection: UserSourceConnection,
|
||||
) -> HttpResponse:
|
||||
"""User was not authenticated and previous request was not authenticated."""
|
||||
messages.success(
|
||||
self.request,
|
||||
_("Successfully authenticated with %(source)s!" % {"source": self.source.name}),
|
||||
)
|
||||
|
||||
# We run the Flow planner here so we can pass the Pending user in the context
|
||||
if not self.source.enrollment_flow:
|
||||
self._logger.warning("source has no enrollment flow")
|
||||
return HttpResponseBadRequest()
|
||||
return self._handle_login_flow(
|
||||
return bad_request_message(
|
||||
self.request,
|
||||
_("Source is not configured for enrollment."),
|
||||
)
|
||||
return self._prepare_flow(
|
||||
self.source.enrollment_flow,
|
||||
connection,
|
||||
stages=[
|
||||
in_memory_stage(
|
||||
MessageStage,
|
||||
message=_(
|
||||
"Successfully authenticated with %(source)s!" % {"source": self.source.name}
|
||||
),
|
||||
)
|
||||
],
|
||||
**{
|
||||
PLAN_CONTEXT_PROMPT: delete_none_keys(self.enroll_info),
|
||||
PLAN_CONTEXT_USER_PATH: self.source.get_user_path(),
|
||||
|
@ -1,6 +1,9 @@
|
||||
{% load i18n %}
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<script>
|
||||
window.authentik = {};
|
||||
window.authentik.locale = "{{ tenant.default_locale }}";
|
||||
window.authentik.locale = "{{ LANGUAGE_CODE }}";
|
||||
window.authentik.config = JSON.parse('{{ config_json|escapejs }}');
|
||||
window.authentik.tenant = JSON.parse('{{ tenant_json|escapejs }}');
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
|
@ -13,8 +13,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-interface-admin data-refresh-on-locale="true">
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-admin>
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
|
@ -29,8 +29,8 @@ window.authentik.flow = {
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-flow-executor data-refresh-on-locale="true">
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-flow-executor>
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
|
@ -13,8 +13,8 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<ak-message-container data-refresh-on-locale="true"></ak-message-container>
|
||||
<ak-interface-user data-refresh-on-locale="true">
|
||||
<ak-message-container></ak-message-container>
|
||||
<ak-interface-user>
|
||||
<section class="ak-static-page pf-c-page__main-section pf-m-no-padding-mobile pf-m-xl">
|
||||
<div class="pf-c-empty-state" style="height: 100vh;">
|
||||
<div class="pf-c-empty-state__content">
|
||||
|
@ -445,8 +445,9 @@ class NotificationTransport(SerializerModel):
|
||||
subject += notification.body[:75]
|
||||
mail = TemplateEmailMessage(
|
||||
subject=subject,
|
||||
template_name="email/generic.html",
|
||||
to=[notification.user.email],
|
||||
language=notification.user.locale(),
|
||||
template_name="email/generic.html",
|
||||
template_context={
|
||||
"title": subject,
|
||||
"body": notification.body,
|
||||
|
@ -72,6 +72,7 @@ class FlowSerializer(ModelSerializer):
|
||||
"export_url",
|
||||
"layout",
|
||||
"denied_action",
|
||||
"authentication",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"background": {"read_only": True},
|
||||
|
@ -1,4 +1,6 @@
|
||||
"""flow exceptions"""
|
||||
from typing import Optional
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from authentik.lib.sentry import SentryIgnoredException
|
||||
@ -6,15 +8,15 @@ from authentik.policies.types import PolicyResult
|
||||
|
||||
|
||||
class FlowNonApplicableException(SentryIgnoredException):
|
||||
"""Flow does not apply to current user (denied by policy)."""
|
||||
"""Flow does not apply to current user (denied by policy, or otherwise)."""
|
||||
|
||||
policy_result: PolicyResult
|
||||
policy_result: Optional[PolicyResult] = None
|
||||
|
||||
@property
|
||||
def messages(self) -> str:
|
||||
"""Get messages from policy result, fallback to generic reason"""
|
||||
if len(self.policy_result.messages) < 1:
|
||||
return _("Flow does not apply to current user (denied by policy).")
|
||||
if not self.policy_result or len(self.policy_result.messages) < 1:
|
||||
return _("Flow does not apply to current user.")
|
||||
return "\n".join(self.policy_result.messages)
|
||||
|
||||
|
||||
|
27
authentik/flows/migrations/0024_flow_authentication.py
Normal file
27
authentik/flows/migrations/0024_flow_authentication.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Generated by Django 4.1.3 on 2022-11-30 09:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0023_flow_denied_action"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="flow",
|
||||
name="authentication",
|
||||
field=models.TextField(
|
||||
choices=[
|
||||
("none", "None"),
|
||||
("require_authenticated", "Require Authenticated"),
|
||||
("require_unauthenticated", "Require Unauthenticated"),
|
||||
("require_superuser", "Require Superuser"),
|
||||
],
|
||||
default="none",
|
||||
help_text="Required level of authentication and authorization to access a flow.",
|
||||
),
|
||||
),
|
||||
]
|
@ -23,6 +23,15 @@ if TYPE_CHECKING:
|
||||
LOGGER = get_logger()
|
||||
|
||||
|
||||
class FlowAuthenticationRequirement(models.TextChoices):
|
||||
"""Required level of authentication and authorization to access a flow"""
|
||||
|
||||
NONE = "none"
|
||||
REQUIRE_AUTHENTICATED = "require_authenticated"
|
||||
REQUIRE_UNAUTHENTICATED = "require_unauthenticated"
|
||||
REQUIRE_SUPERUSER = "require_superuser"
|
||||
|
||||
|
||||
class NotConfiguredAction(models.TextChoices):
|
||||
"""Decides how the FlowExecutor should proceed when a stage isn't configured"""
|
||||
|
||||
@ -152,6 +161,12 @@ class Flow(SerializerModel, PolicyBindingModel):
|
||||
help_text=_("Configure what should happen when a flow denies access to a user."),
|
||||
)
|
||||
|
||||
authentication = models.TextField(
|
||||
choices=FlowAuthenticationRequirement.choices,
|
||||
default=FlowAuthenticationRequirement.NONE,
|
||||
help_text=_("Required level of authentication and authorization to access a flow."),
|
||||
)
|
||||
|
||||
@property
|
||||
def background_url(self) -> str:
|
||||
"""Get the URL to the background image. If the name is /static or starts with http
|
||||
|
@ -13,7 +13,14 @@ from authentik.events.models import cleanse_dict
|
||||
from authentik.flows.apps import HIST_FLOWS_PLAN_TIME
|
||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import Flow, FlowDesignation, FlowStageBinding, Stage, in_memory_stage
|
||||
from authentik.flows.models import (
|
||||
Flow,
|
||||
FlowAuthenticationRequirement,
|
||||
FlowDesignation,
|
||||
FlowStageBinding,
|
||||
Stage,
|
||||
in_memory_stage,
|
||||
)
|
||||
from authentik.lib.config import CONFIG
|
||||
from authentik.policies.engine import PolicyEngine
|
||||
|
||||
@ -116,11 +123,30 @@ class FlowPlanner:
|
||||
self.flow = flow
|
||||
self._logger = get_logger().bind(flow_slug=flow.slug)
|
||||
|
||||
def _check_authentication(self, request: HttpRequest):
|
||||
"""Check the flow's authentication level is matched by `request`"""
|
||||
if (
|
||||
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
|
||||
and not request.user.is_authenticated
|
||||
):
|
||||
raise FlowNonApplicableException()
|
||||
if (
|
||||
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_UNAUTHENTICATED
|
||||
and request.user.is_authenticated
|
||||
):
|
||||
raise FlowNonApplicableException()
|
||||
if (
|
||||
self.flow.authentication == FlowAuthenticationRequirement.REQUIRE_SUPERUSER
|
||||
and not request.user.is_superuser
|
||||
):
|
||||
raise FlowNonApplicableException()
|
||||
|
||||
def plan(
|
||||
self, request: HttpRequest, default_context: Optional[dict[str, Any]] = None
|
||||
) -> FlowPlan:
|
||||
"""Check each of the flows' policies, check policies for each stage with PolicyBinding
|
||||
and return ordered list"""
|
||||
self._check_authentication(request)
|
||||
with Hub.current.start_span(
|
||||
op="authentik.flow.planner.plan", description=self.flow.slug
|
||||
) as span:
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""flow planner tests"""
|
||||
from unittest.mock import MagicMock, Mock, PropertyMock, patch
|
||||
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.sessions.middleware import SessionMiddleware
|
||||
from django.core.cache import cache
|
||||
from django.test import RequestFactory, TestCase
|
||||
@ -8,10 +9,10 @@ from django.urls import reverse
|
||||
from guardian.shortcuts import get_anonymous_user
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.core.tests.utils import create_test_flow
|
||||
from authentik.core.tests.utils import create_test_admin_user, create_test_flow
|
||||
from authentik.flows.exceptions import EmptyFlowException, FlowNonApplicableException
|
||||
from authentik.flows.markers import ReevaluateMarker, StageMarker
|
||||
from authentik.flows.models import FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.models import FlowAuthenticationRequirement, FlowDesignation, FlowStageBinding
|
||||
from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlanner, cache_key
|
||||
from authentik.lib.tests.utils import dummy_get_response
|
||||
from authentik.policies.dummy.models import DummyPolicy
|
||||
@ -43,6 +44,30 @@ class TestFlowPlanner(TestCase):
|
||||
planner = FlowPlanner(flow)
|
||||
planner.plan(request)
|
||||
|
||||
def test_authentication(self):
|
||||
"""Test flow authentication"""
|
||||
flow = create_test_flow()
|
||||
flow.authentication = FlowAuthenticationRequirement.NONE
|
||||
request = self.request_factory.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": flow.slug}),
|
||||
)
|
||||
request.user = AnonymousUser()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
planner.plan(request)
|
||||
|
||||
with self.assertRaises(FlowNonApplicableException):
|
||||
flow.authentication = FlowAuthenticationRequirement.REQUIRE_AUTHENTICATED
|
||||
FlowPlanner(flow).plan(request)
|
||||
with self.assertRaises(FlowNonApplicableException):
|
||||
flow.authentication = FlowAuthenticationRequirement.REQUIRE_SUPERUSER
|
||||
FlowPlanner(flow).plan(request)
|
||||
|
||||
request.user = create_test_admin_user()
|
||||
planner = FlowPlanner(flow)
|
||||
planner.allow_empty_flows = True
|
||||
planner.plan(request)
|
||||
|
||||
@patch(
|
||||
"authentik.policies.engine.PolicyEngine.result",
|
||||
POLICY_RETURN_FALSE,
|
||||
|
@ -253,9 +253,9 @@ class FlowExecutorView(APIView):
|
||||
action=EventAction.SYSTEM_EXCEPTION,
|
||||
message=exception_to_string(exc),
|
||||
).from_http(self.request)
|
||||
return to_stage_response(
|
||||
self.request, HttpChallengeResponse(FlowErrorChallenge(self.request, exc))
|
||||
)
|
||||
challenge = FlowErrorChallenge(self.request, exc)
|
||||
challenge.is_valid()
|
||||
return to_stage_response(self.request, HttpChallengeResponse(challenge))
|
||||
|
||||
@extend_schema(
|
||||
responses={
|
||||
|
@ -216,6 +216,7 @@ class ResponseProcessor:
|
||||
# Flatten all lists in the dict
|
||||
for key, value in attributes.items():
|
||||
attributes[key] = BaseEvaluator.expr_flatten(value)
|
||||
attributes["username"] = self._get_name_id().text
|
||||
return attributes
|
||||
|
||||
def prepare_flow_manager(self) -> SourceFlowManager:
|
||||
|
@ -109,4 +109,7 @@ class TestResponseProcessor(TestCase):
|
||||
parser = ResponseProcessor(self.source, request)
|
||||
parser.parse()
|
||||
sfm = parser.prepare_flow_manager()
|
||||
self.assertEqual(sfm.enroll_info, {"email": "foo@bar.baz", "name": "foo", "sn": "bar"})
|
||||
self.assertEqual(
|
||||
sfm.enroll_info,
|
||||
{"email": "foo@bar.baz", "name": "foo", "sn": "bar", "username": "jens@beryju.org"},
|
||||
)
|
||||
|
@ -28,8 +28,8 @@ class Command(BaseCommand):
|
||||
delete_stage = True
|
||||
message = TemplateEmailMessage(
|
||||
subject="authentik Test-Email",
|
||||
template_name="email/setup.html",
|
||||
to=[options["to"]],
|
||||
template_name="email/setup.html",
|
||||
template_context={},
|
||||
)
|
||||
try:
|
||||
|
@ -11,6 +11,7 @@ from django.utils.translation import gettext as _
|
||||
from rest_framework.fields import CharField
|
||||
from rest_framework.serializers import ValidationError
|
||||
|
||||
from authentik.core.models import User
|
||||
from authentik.flows.challenge import Challenge, ChallengeResponse, ChallengeTypes
|
||||
from authentik.flows.models import FlowToken
|
||||
from authentik.flows.planner import PLAN_CONTEXT_IS_RESTORED, PLAN_CONTEXT_PENDING_USER
|
||||
@ -81,7 +82,7 @@ class EmailStageView(ChallengeStageView):
|
||||
def send_email(self):
|
||||
"""Helper function that sends the actual email. Implies that you've
|
||||
already checked that there is a pending user."""
|
||||
pending_user = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
pending_user: User = self.executor.plan.context[PLAN_CONTEXT_PENDING_USER]
|
||||
email = self.executor.plan.context.get(PLAN_CONTEXT_EMAIL_OVERRIDE, None)
|
||||
if not email:
|
||||
email = pending_user.email
|
||||
@ -90,8 +91,9 @@ class EmailStageView(ChallengeStageView):
|
||||
# Send mail to user
|
||||
message = TemplateEmailMessage(
|
||||
subject=_(current_stage.subject),
|
||||
template_name=current_stage.template,
|
||||
to=[email],
|
||||
language=pending_user.locale(self.request),
|
||||
template_name=current_stage.template,
|
||||
template_context={
|
||||
"url": self.get_full_url(**{QS_KEY_TOKEN: token.key}),
|
||||
"user": pending_user,
|
||||
|
@ -1,13 +1,15 @@
|
||||
"""email utils"""
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils import translation
|
||||
|
||||
|
||||
class TemplateEmailMessage(EmailMultiAlternatives):
|
||||
"""Wrapper around EmailMultiAlternatives with integrated template rendering"""
|
||||
|
||||
def __init__(self, template_name=None, template_context=None, **kwargs):
|
||||
html_content = render_to_string(template_name, template_context)
|
||||
def __init__(self, template_name=None, template_context=None, language="", **kwargs):
|
||||
with translation.override(language):
|
||||
html_content = render_to_string(template_name, template_context)
|
||||
super().__init__(**kwargs)
|
||||
self.content_subtype = "html"
|
||||
self.attach_alternative(html_content, "text/html")
|
||||
|
@ -8,6 +8,7 @@ from rest_framework.viewsets import ModelViewSet
|
||||
from authentik.core.api.groups import GroupMemberSerializer
|
||||
from authentik.core.api.used_by import UsedByMixin
|
||||
from authentik.core.api.utils import is_dict
|
||||
from authentik.flows.api.flows import FlowSerializer
|
||||
from authentik.flows.api.stages import StageSerializer
|
||||
from authentik.stages.invitation.models import Invitation, InvitationStage
|
||||
|
||||
@ -49,6 +50,7 @@ class InvitationSerializer(ModelSerializer):
|
||||
|
||||
created_by = GroupMemberSerializer(read_only=True)
|
||||
fixed_data = JSONField(validators=[is_dict], required=False)
|
||||
flow_obj = FlowSerializer(read_only=True, required=False, source="flow")
|
||||
|
||||
class Meta:
|
||||
|
||||
@ -60,6 +62,8 @@ class InvitationSerializer(ModelSerializer):
|
||||
"fixed_data",
|
||||
"created_by",
|
||||
"single_use",
|
||||
"flow",
|
||||
"flow_obj",
|
||||
]
|
||||
|
||||
|
||||
@ -69,8 +73,8 @@ class InvitationViewSet(UsedByMixin, ModelViewSet):
|
||||
queryset = Invitation.objects.all()
|
||||
serializer_class = InvitationSerializer
|
||||
ordering = ["-expires"]
|
||||
search_fields = ["name", "created_by__username", "expires"]
|
||||
filterset_fields = ["name", "created_by__username", "expires"]
|
||||
search_fields = ["name", "created_by__username", "expires", "flow__slug"]
|
||||
filterset_fields = ["name", "created_by__username", "expires", "flow__slug"]
|
||||
|
||||
def perform_create(self, serializer: InvitationSerializer):
|
||||
serializer.save(created_by=self.request.user)
|
||||
|
@ -0,0 +1,26 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-20 13:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_flows", "0024_flow_authentication"),
|
||||
("authentik_stages_invitation", "0001_squashed_0006_invitation_name"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="invitation",
|
||||
name="flow",
|
||||
field=models.ForeignKey(
|
||||
default=None,
|
||||
help_text="When set, only the configured flow can use this invitation.",
|
||||
null=True,
|
||||
on_delete=django.db.models.deletion.SET_DEFAULT,
|
||||
to="authentik_flows.flow",
|
||||
),
|
||||
),
|
||||
]
|
@ -55,6 +55,13 @@ class Invitation(SerializerModel, ExpiringModel):
|
||||
|
||||
name = models.SlugField()
|
||||
|
||||
flow = models.ForeignKey(
|
||||
"authentik_flows.Flow",
|
||||
default=None,
|
||||
null=True,
|
||||
on_delete=models.SET_DEFAULT,
|
||||
help_text=_("When set, only the configured flow can use this invitation."),
|
||||
)
|
||||
single_use = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("When enabled, the invitation will be deleted after usage."),
|
||||
|
@ -37,22 +37,30 @@ class InvitationStageView(StageView):
|
||||
return self.executor.plan.context[PLAN_CONTEXT_PROMPT][INVITATION_TOKEN_KEY_CONTEXT]
|
||||
return None
|
||||
|
||||
def get_invite(self) -> Optional[Invitation]:
|
||||
"""Check the token, find the invite and check it's flow"""
|
||||
token = self.get_token()
|
||||
if not token:
|
||||
return None
|
||||
invite: Invitation = Invitation.objects.filter(pk=token).first()
|
||||
if not invite:
|
||||
self.logger.debug("invalid invitation", token=token)
|
||||
return None
|
||||
if invite.flow and invite.flow.pk != self.executor.plan.flow_pk:
|
||||
self.logger.debug("invite for incorrect flow", expected=invite.flow.slug)
|
||||
return None
|
||||
return invite
|
||||
|
||||
def get(self, request: HttpRequest) -> HttpResponse:
|
||||
"""Apply data to the current flow based on a URL"""
|
||||
stage: InvitationStage = self.executor.current_stage
|
||||
token = self.get_token()
|
||||
if not token:
|
||||
# No Invitation was given, raise error or continue
|
||||
|
||||
invite = self.get_invite()
|
||||
if not invite:
|
||||
if stage.continue_flow_without_invitation:
|
||||
return self.executor.stage_ok()
|
||||
return self.executor.stage_invalid()
|
||||
|
||||
invite: Invitation = Invitation.objects.filter(pk=token).first()
|
||||
if not invite:
|
||||
self.logger.debug("invalid invitation", token=token)
|
||||
if stage.continue_flow_without_invitation:
|
||||
return self.executor.stage_ok()
|
||||
return self.executor.stage_invalid()
|
||||
self.executor.plan.context[INVITATION_IN_EFFECT] = True
|
||||
self.executor.plan.context[INVITATION] = invite
|
||||
|
||||
|
@ -23,7 +23,7 @@ from authentik.stages.password import BACKEND_INBUILT
|
||||
from authentik.stages.password.stage import PLAN_CONTEXT_AUTHENTICATION_BACKEND
|
||||
|
||||
|
||||
class TestUserLoginStage(FlowTestCase):
|
||||
class TestInvitationStage(FlowTestCase):
|
||||
"""Login tests"""
|
||||
|
||||
def setUp(self):
|
||||
@ -98,6 +98,33 @@ class TestUserLoginStage(FlowTestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
|
||||
def test_invalid_flow(self):
|
||||
"""Test with invitation, invalid flow limit"""
|
||||
invalid_flow = create_test_flow(FlowDesignation.ENROLLMENT)
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
session = self.client.session
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
data = {"foo": "bar"}
|
||||
invite = Invitation.objects.create(
|
||||
created_by=get_anonymous_user(), fixed_data=data, flow=invalid_flow
|
||||
)
|
||||
|
||||
with patch("authentik.flows.views.executor.FlowExecutorView.cancel", MagicMock()):
|
||||
base_url = reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
args = urlencode({INVITATION_TOKEN_KEY: invite.pk.hex})
|
||||
response = self.client.get(base_url + f"?query={args}")
|
||||
|
||||
session = self.client.session
|
||||
plan: FlowPlan = session[SESSION_KEY_PLAN]
|
||||
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
flow=self.flow,
|
||||
component="ak-stage-access-denied",
|
||||
)
|
||||
|
||||
def test_with_invitation_prompt_data(self):
|
||||
"""Test with invitation, check data in session"""
|
||||
data = {"foo": "bar"}
|
||||
|
@ -15,6 +15,7 @@ class UserWriteStageSerializer(StageSerializer):
|
||||
fields = StageSerializer.Meta.fields + [
|
||||
"create_users_as_inactive",
|
||||
"create_users_group",
|
||||
"can_create_users",
|
||||
"user_path_template",
|
||||
]
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
# Generated by Django 4.1.4 on 2022-12-22 14:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("authentik_stages_user_write", "0005_userwritestage_user_path_template"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="userwritestage",
|
||||
name="can_create_users",
|
||||
field=models.BooleanField(
|
||||
default=True,
|
||||
help_text="When set, this stage can create users. If not enabled and no user is available, stage will fail.",
|
||||
),
|
||||
),
|
||||
]
|
@ -13,6 +13,16 @@ class UserWriteStage(Stage):
|
||||
"""Writes currently pending data into the pending user, or if no user exists,
|
||||
creates a new user with the data."""
|
||||
|
||||
can_create_users = models.BooleanField(
|
||||
default=True,
|
||||
help_text=_(
|
||||
(
|
||||
"When set, this stage can create users. "
|
||||
"If not enabled and no user is available, stage will fail."
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
create_users_as_inactive = models.BooleanField(
|
||||
default=False,
|
||||
help_text=_("When set, newly created users are inactive and cannot login."),
|
||||
|
@ -1,10 +1,9 @@
|
||||
"""Write stage logic"""
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
from django.db import transaction
|
||||
from django.db.utils import IntegrityError
|
||||
from django.db.utils import IntegrityError, InternalError
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
@ -47,7 +46,7 @@ class UserWriteStageView(StageView):
|
||||
"""Wrapper for post requests"""
|
||||
return self.get(request)
|
||||
|
||||
def ensure_user(self) -> tuple[User, bool]:
|
||||
def ensure_user(self) -> tuple[Optional[User], bool]:
|
||||
"""Ensure a user exists"""
|
||||
user_created = False
|
||||
path = self.executor.plan.context.get(
|
||||
@ -55,7 +54,11 @@ class UserWriteStageView(StageView):
|
||||
)
|
||||
if path == "":
|
||||
path = User.default_path()
|
||||
if not self.request.user.is_anonymous:
|
||||
self.executor.plan.context.setdefault(PLAN_CONTEXT_PENDING_USER, self.request.user)
|
||||
if PLAN_CONTEXT_PENDING_USER not in self.executor.plan.context:
|
||||
if not self.executor.current_stage.can_create_users:
|
||||
return None, False
|
||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = User(
|
||||
is_active=not self.executor.current_stage.create_users_as_inactive,
|
||||
path=path,
|
||||
@ -110,11 +113,14 @@ class UserWriteStageView(StageView):
|
||||
a new user is created."""
|
||||
if PLAN_CONTEXT_PROMPT not in self.executor.plan.context:
|
||||
message = _("No Pending data.")
|
||||
messages.error(request, message)
|
||||
self.logger.debug(message)
|
||||
return self.executor.stage_invalid()
|
||||
return self.executor.stage_invalid(message)
|
||||
data = self.executor.plan.context[PLAN_CONTEXT_PROMPT]
|
||||
user, user_created = self.ensure_user()
|
||||
if not user:
|
||||
message = _("No user found and can't create new user.")
|
||||
self.logger.info(message)
|
||||
return self.executor.stage_invalid(message)
|
||||
# Before we change anything, check if the user is the same as in the request
|
||||
# and we're updating a password. In that case we need to update the session hash
|
||||
# Also check that we're not currently impersonating, so we don't update the session
|
||||
@ -137,9 +143,9 @@ class UserWriteStageView(StageView):
|
||||
user.ak_groups.add(self.executor.current_stage.create_users_group)
|
||||
if PLAN_CONTEXT_GROUPS in self.executor.plan.context:
|
||||
user.ak_groups.add(*self.executor.plan.context[PLAN_CONTEXT_GROUPS])
|
||||
except (IntegrityError, ValueError, TypeError) as exc:
|
||||
except (IntegrityError, ValueError, TypeError, InternalError) as exc:
|
||||
self.logger.warning("Failed to save user", exc=exc)
|
||||
return self.executor.stage_invalid()
|
||||
return self.executor.stage_invalid(_("Failed to save user"))
|
||||
user_write.send(sender=self, request=request, user=user, data=data, created=user_created)
|
||||
# Check if the password has been updated, and update the session auth hash
|
||||
if should_update_session:
|
||||
|
@ -1,6 +1,4 @@
|
||||
"""write tests"""
|
||||
import string
|
||||
from random import SystemRandom
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.urls import reverse
|
||||
@ -14,6 +12,7 @@ from authentik.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||
from authentik.flows.tests import FlowTestCase
|
||||
from authentik.flows.tests.test_executor import TO_STAGE_RESPONSE_MOCK
|
||||
from authentik.flows.views.executor import SESSION_KEY_PLAN
|
||||
from authentik.lib.generators import generate_key
|
||||
from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
|
||||
from authentik.stages.user_write.models import UserWriteStage
|
||||
from authentik.stages.user_write.stage import PLAN_CONTEXT_GROUPS, UserWriteStageView
|
||||
@ -32,12 +31,11 @@ class TestUserWriteStage(FlowTestCase):
|
||||
)
|
||||
self.binding = FlowStageBinding.objects.create(target=self.flow, stage=self.stage, order=2)
|
||||
self.source = Source.objects.create(name="fake_source")
|
||||
self.user = create_test_admin_user()
|
||||
|
||||
def test_user_create(self):
|
||||
"""Test creation of user"""
|
||||
password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8)
|
||||
)
|
||||
password = generate_key()
|
||||
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
@ -66,9 +64,7 @@ class TestUserWriteStage(FlowTestCase):
|
||||
|
||||
def test_user_update(self):
|
||||
"""Test update of existing user"""
|
||||
new_password = "".join(
|
||||
SystemRandom().choice(string.ascii_uppercase + string.digits) for _ in range(8)
|
||||
)
|
||||
new_password = generate_key()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
plan.context[PLAN_CONTEXT_PENDING_USER] = User.objects.create(
|
||||
username="unittest", email="test@goauthentik.io"
|
||||
@ -142,6 +138,49 @@ class TestUserWriteStage(FlowTestCase):
|
||||
component="ak-stage-access-denied",
|
||||
)
|
||||
|
||||
def test_authenticated_no_user(self):
|
||||
"""Test user in session and none in plan"""
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
self.client.force_login(self.user)
|
||||
session = self.client.session
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
"username": "foo",
|
||||
"attribute_some-custom-attribute": "test",
|
||||
"some_ignored_attribute": "bar",
|
||||
}
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
self.assertStageRedirects(response, reverse("authentik_core:root-redirect"))
|
||||
self.user.refresh_from_db()
|
||||
self.assertEqual(self.user.username, "foo")
|
||||
|
||||
def test_no_create(self):
|
||||
"""Test can_create_users set to false"""
|
||||
self.stage.can_create_users = False
|
||||
self.stage.save()
|
||||
plan = FlowPlan(flow_pk=self.flow.pk.hex, bindings=[self.binding], markers=[StageMarker()])
|
||||
session = self.client.session
|
||||
plan.context[PLAN_CONTEXT_PROMPT] = {
|
||||
"username": "foo",
|
||||
"attribute_some-custom-attribute": "test",
|
||||
"some_ignored_attribute": "bar",
|
||||
}
|
||||
session[SESSION_KEY_PLAN] = plan
|
||||
session.save()
|
||||
|
||||
response = self.client.get(
|
||||
reverse("authentik_api:flow-executor", kwargs={"flow_slug": self.flow.slug})
|
||||
)
|
||||
self.assertStageResponse(
|
||||
response,
|
||||
self.flow,
|
||||
component="ak-stage-access-denied",
|
||||
)
|
||||
|
||||
@patch(
|
||||
"authentik.flows.views.executor.to_stage_response",
|
||||
TO_STAGE_RESPONSE_MOCK,
|
||||
|
@ -3,6 +3,7 @@ from typing import Callable
|
||||
|
||||
from django.http.request import HttpRequest
|
||||
from django.http.response import HttpResponse
|
||||
from django.utils.translation import activate
|
||||
from sentry_sdk.api import set_tag
|
||||
|
||||
from authentik.tenants.utils import get_tenant_for_request
|
||||
@ -22,4 +23,7 @@ class TenantMiddleware:
|
||||
setattr(request, "tenant", tenant)
|
||||
set_tag("authentik.tenant_uuid", tenant.tenant_uuid.hex)
|
||||
set_tag("authentik.tenant_domain", tenant.domain)
|
||||
locale = tenant.default_locale
|
||||
if locale != "":
|
||||
activate(locale)
|
||||
return self.get_response(request)
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: stage_configuration
|
||||
name: Change Password
|
||||
title: Change password
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-password-change
|
||||
model: authentik_flows.flow
|
||||
@ -44,6 +45,8 @@ entries:
|
||||
name: default-password-change-write
|
||||
id: default-password-change-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs:
|
||||
can_create_users: false
|
||||
- identifiers:
|
||||
order: 0
|
||||
stage: !KeyOf default-password-change-prompt
|
||||
|
@ -11,6 +11,7 @@ entries:
|
||||
designation: authentication
|
||||
name: Welcome to authentik!
|
||||
title: Welcome to authentik!
|
||||
authentication: require_unauthenticated
|
||||
identifiers:
|
||||
slug: default-authentication-flow
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: invalidation
|
||||
name: Logout
|
||||
title: Default Invalidation Flow
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-invalidation-flow
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: stage_configuration
|
||||
name: default-authenticator-static-setup
|
||||
title: Setup Static OTP Tokens
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-authenticator-static-setup
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: stage_configuration
|
||||
name: default-authenticator-totp-setup
|
||||
title: Setup Two-Factor authentication
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-authenticator-totp-setup
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: stage_configuration
|
||||
name: default-authenticator-webauthn-setup
|
||||
title: Setup WebAuthn
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-authenticator-webauthn-setup
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: authorization
|
||||
name: Authorize Application
|
||||
title: Redirecting to %(app)s
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-provider-authorization-explicit-consent
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: authorization
|
||||
name: Authorize Application
|
||||
title: Redirecting to %(app)s
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-provider-authorization-implicit-consent
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: authentication
|
||||
name: Welcome to authentik!
|
||||
title: Welcome to authentik!
|
||||
authentication: require_unauthenticated
|
||||
identifiers:
|
||||
slug: default-source-authentication
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: enrollment
|
||||
name: Welcome to authentik! Please select a username.
|
||||
title: Welcome to authentik! Please select a username.
|
||||
authentication: none
|
||||
identifiers:
|
||||
slug: default-source-enrollment
|
||||
model: authentik_flows.flow
|
||||
@ -56,6 +57,8 @@ entries:
|
||||
name: default-source-enrollment-write
|
||||
id: default-source-enrollment-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs:
|
||||
can_create_users: true
|
||||
- attrs:
|
||||
re_evaluate_policies: true
|
||||
identifiers:
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: stage_configuration
|
||||
name: Pre-Authentication
|
||||
title: Pre-authentication
|
||||
authentication: none
|
||||
identifiers:
|
||||
slug: default-source-pre-authentication
|
||||
model: authentik_flows.flow
|
||||
|
@ -6,6 +6,7 @@ entries:
|
||||
designation: stage_configuration
|
||||
name: User settings
|
||||
title: Update your info
|
||||
authentication: require_authenticated
|
||||
identifiers:
|
||||
slug: default-user-settings-flow
|
||||
model: authentik_flows.flow
|
||||
@ -108,6 +109,8 @@ entries:
|
||||
model: authentik_policies_expression.expressionpolicy
|
||||
- identifiers:
|
||||
name: default-user-settings-write
|
||||
attrs:
|
||||
can_create_users: false
|
||||
id: default-user-settings-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
- attrs:
|
||||
|
@ -102,6 +102,8 @@ entries:
|
||||
identifiers:
|
||||
name: default-password-change-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs:
|
||||
can_create_users: false
|
||||
- attrs:
|
||||
evaluate_on_plan: true
|
||||
invalid_response_action: retry
|
||||
|
@ -12,6 +12,7 @@ entries:
|
||||
name: Default enrollment Flow
|
||||
title: Welcome to authentik!
|
||||
designation: enrollment
|
||||
authentication: require_unauthenticated
|
||||
- identifiers:
|
||||
field_key: username
|
||||
label: Username
|
||||
@ -94,7 +95,8 @@ entries:
|
||||
name: default-enrollment-user-write
|
||||
id: default-enrollment-user-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs: {}
|
||||
attrs:
|
||||
can_create_users: true
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf default-enrollment-prompt-first
|
||||
|
@ -12,6 +12,7 @@ entries:
|
||||
name: Default enrollment Flow
|
||||
title: Welcome to authentik!
|
||||
designation: enrollment
|
||||
authentication: require_unauthenticated
|
||||
- identifiers:
|
||||
field_key: username
|
||||
label: Username
|
||||
@ -113,6 +114,7 @@ entries:
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs:
|
||||
create_users_as_inactive: true
|
||||
can_create_users: true
|
||||
- identifiers:
|
||||
target: !KeyOf flow
|
||||
stage: !KeyOf default-enrollment-prompt-first
|
||||
|
@ -12,6 +12,7 @@ entries:
|
||||
name: Default Authentication Flow
|
||||
title: Welcome to authentik!
|
||||
designation: authentication
|
||||
authentication: require_unauthenticated
|
||||
- identifiers:
|
||||
name: test-not-app-password
|
||||
id: test-not-app-password
|
||||
|
@ -12,6 +12,7 @@ entries:
|
||||
name: Default Authentication Flow
|
||||
title: Welcome to authentik!
|
||||
designation: authentication
|
||||
authentication: require_unauthenticated
|
||||
- identifiers:
|
||||
name: default-authentication-login
|
||||
id: default-authentication-login
|
||||
|
@ -12,6 +12,7 @@ entries:
|
||||
name: Default recovery flow
|
||||
title: Reset your password
|
||||
designation: recovery
|
||||
authentication: require_unauthenticated
|
||||
- identifiers:
|
||||
field_key: password
|
||||
label: Password
|
||||
@ -62,6 +63,8 @@ entries:
|
||||
name: default-recovery-user-write
|
||||
id: default-recovery-user-write
|
||||
model: authentik_stages_user_write.userwritestage
|
||||
attrs:
|
||||
can_create_users: false
|
||||
- identifiers:
|
||||
name: default-recovery-identification
|
||||
id: default-recovery-identification
|
||||
|
@ -12,6 +12,7 @@ entries:
|
||||
name: Default unenrollment flow
|
||||
title: Delete your account
|
||||
designation: unenrollment
|
||||
authentication: require_authenticated
|
||||
- identifiers:
|
||||
name: default-unenrollment-user-delete
|
||||
id: default-unenrollment-user-delete
|
||||
|
@ -32,7 +32,7 @@ services:
|
||||
volumes:
|
||||
- redis:/data
|
||||
server:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.10.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.10.4}
|
||||
restart: unless-stopped
|
||||
command: server
|
||||
environment:
|
||||
@ -52,7 +52,7 @@ services:
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTP:-9000}:9000"
|
||||
- "0.0.0.0:${AUTHENTIK_PORT_HTTPS:-9443}:9443"
|
||||
worker:
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.10.0}
|
||||
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2022.10.4}
|
||||
restart: unless-stopped
|
||||
command: worker
|
||||
environment:
|
||||
|
4
go.mod
4
go.mod
@ -24,8 +24,8 @@ require (
|
||||
github.com/prometheus/client_golang v1.13.0
|
||||
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b
|
||||
github.com/sirupsen/logrus v1.9.0
|
||||
github.com/stretchr/testify v1.8.0
|
||||
goauthentik.io/api/v3 v3.2022090.10
|
||||
github.com/stretchr/testify v1.8.1
|
||||
goauthentik.io/api/v3 v3.2022100.1
|
||||
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||
gopkg.in/boj/redistore.v1 v1.0.0-20160128113310-fc113767cd6b
|
||||
|
8
go.sum
8
go.sum
@ -342,6 +342,7 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
@ -349,8 +350,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
@ -373,8 +375,8 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
|
||||
goauthentik.io/api/v3 v3.2022090.10 h1:2eg2ay80xGjg4rNb704tUceLyWXezjIsKmFXGNnJDgA=
|
||||
goauthentik.io/api/v3 v3.2022090.10/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
|
||||
goauthentik.io/api/v3 v3.2022100.1 h1:9QmSNmLZmNlIyAWSmG2YpQKtr5LsoDopPsRxwbyzANI=
|
||||
goauthentik.io/api/v3 v3.2022100.1/go.mod h1:QM9J32HgYE4gL71lWAfAoXSPdSmLVLW08itfLI3Mo10=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
|
||||
|
@ -29,4 +29,4 @@ func UserAgent() string {
|
||||
return fmt.Sprintf("authentik@%s", FullVersion())
|
||||
}
|
||||
|
||||
const VERSION = "2022.10.0"
|
||||
const VERSION = "2022.10.4"
|
||||
|
212
poetry.lock
generated
212
poetry.lock
generated
@ -78,7 +78,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "astroid"
|
||||
version = "2.12.11"
|
||||
version = "2.12.12"
|
||||
description = "An abstract syntax tree for Python with inference support."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -442,11 +442,11 @@ toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "constantly"
|
||||
@ -518,7 +518,7 @@ tests = ["django", "hypothesis", "pytest", "pytest-asyncio"]
|
||||
|
||||
[[package]]
|
||||
name = "deepmerge"
|
||||
version = "1.0.1"
|
||||
version = "1.1.0"
|
||||
description = "a toolset to deeply merge python dictionaries."
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -738,7 +738,7 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "duo-client"
|
||||
version = "4.4.0"
|
||||
version = "4.5.0"
|
||||
description = "Reference client for Duo Security APIs"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -748,6 +748,17 @@ python-versions = "*"
|
||||
setuptools = "*"
|
||||
six = "*"
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.0.0rc9"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "facebook-sdk"
|
||||
version = "3.1.0"
|
||||
@ -1031,7 +1042,7 @@ zookeeper = ["kazoo (>=1.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "kubernetes"
|
||||
version = "24.2.0"
|
||||
version = "25.3.0"
|
||||
description = "Kubernetes python client"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -1260,20 +1271,12 @@ wcwidth = "*"
|
||||
|
||||
[[package]]
|
||||
name = "psycopg2-binary"
|
||||
version = "2.9.4"
|
||||
version = "2.9.5"
|
||||
description = "psycopg2 - Python-PostgreSQL Database Adapter"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyasn1"
|
||||
version = "0.4.8"
|
||||
@ -1348,14 +1351,14 @@ tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pylint"
|
||||
version = "2.15.4"
|
||||
version = "2.15.5"
|
||||
description = "python code static checker"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7.2"
|
||||
|
||||
[package.dependencies]
|
||||
astroid = ">=2.12.11,<=2.14.0-dev0"
|
||||
astroid = ">=2.12.12,<=2.14.0-dev0"
|
||||
colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
|
||||
dill = ">=0.2"
|
||||
isort = ">=4.2.5,<6"
|
||||
@ -1454,7 +1457,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.3"
|
||||
version = "7.2.0"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@ -1463,11 +1466,11 @@ python-versions = ">=3.7"
|
||||
[package.dependencies]
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
@ -1637,7 +1640,7 @@ urllib3 = {version = ">=1.26,<2.0", extras = ["socks"]}
|
||||
|
||||
[[package]]
|
||||
name = "sentry-sdk"
|
||||
version = "1.10.0"
|
||||
version = "1.10.1"
|
||||
description = "Python client for Sentry (https://sentry.io)"
|
||||
category = "main"
|
||||
optional = false
|
||||
@ -2241,8 +2244,8 @@ asn1crypto = [
|
||||
{file = "asn1crypto-1.5.1.tar.gz", hash = "sha256:13ae38502be632115abf8a24cbe5f4da52e3b5231990aff31123c805306ccb9c"},
|
||||
]
|
||||
astroid = [
|
||||
{file = "astroid-2.12.11-py3-none-any.whl", hash = "sha256:867a756bbf35b7bc07b35bfa6522acd01f91ad9919df675e8428072869792dce"},
|
||||
{file = "astroid-2.12.11.tar.gz", hash = "sha256:2df4f9980c4511474687895cbfdb8558293c1a826d9118bb09233d7c2bff1c83"},
|
||||
{file = "astroid-2.12.12-py3-none-any.whl", hash = "sha256:72702205200b2a638358369d90c222d74ebc376787af8fb2f7f2a86f7b5cc85f"},
|
||||
{file = "astroid-2.12.12.tar.gz", hash = "sha256:1c00a14f5a3ed0339d38d2e2e5b74ea2591df5861c0936bb292b84ccf3a78d83"},
|
||||
]
|
||||
async-generator = [
|
||||
{file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"},
|
||||
@ -2457,8 +2460,8 @@ codespell = [
|
||||
{file = "codespell-2.2.2.tar.gz", hash = "sha256:c4d00c02b5a2a55661f00d5b4b3b5a710fa803ced9a9d7e45438268b099c319c"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
constantly = [
|
||||
{file = "constantly-15.1.0-py2.py3-none-any.whl", hash = "sha256:dd2fa9d6b1a51a83f0d7dd76293d734046aa176e384bf6e33b7e44880eb37c5d"},
|
||||
@ -2549,8 +2552,8 @@ daphne = [
|
||||
{file = "daphne-4.0.0.tar.gz", hash = "sha256:cce9afc8f49a4f15d4270b8cfb0e0fe811b770a5cc795474e97e4da287497666"},
|
||||
]
|
||||
deepmerge = [
|
||||
{file = "deepmerge-1.0.1-py3-none-any.whl", hash = "sha256:f851fff957697cb8f4580b465acf5c2d35841695306ff0abb9cb9c273ad76112"},
|
||||
{file = "deepmerge-1.0.1.tar.gz", hash = "sha256:4b44779ed3d2fb791bb181fc2683423496fea428abb7af37feb23286de7f0a1a"},
|
||||
{file = "deepmerge-1.1.0-py3-none-any.whl", hash = "sha256:59e6ef80b77dc52af3882a1ea78da22bcfc91ae9cdabc0c80729049fe295ff8b"},
|
||||
{file = "deepmerge-1.1.0.tar.gz", hash = "sha256:4c27a0db5de285e1a7ceac7dbc1531deaa556b627dea4900c8244581ecdfea2d"},
|
||||
]
|
||||
defusedxml = [
|
||||
{file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"},
|
||||
@ -2620,8 +2623,12 @@ dumb-init = [
|
||||
{file = "dumb_init-1.2.5.post1-py2.py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc1e309f52c898ee00df056189f46ca3f6055a72b77015f4eefbd7c58c45290f"},
|
||||
]
|
||||
duo-client = [
|
||||
{file = "duo_client-4.4.0-py2.py3-none-any.whl", hash = "sha256:927b7e838433b20debc8d07c2c418c2e1b650735acb9fcf214eaa3a2caf00358"},
|
||||
{file = "duo_client-4.4.0.tar.gz", hash = "sha256:44e06bf730a201a1e1749215ef16d2c2682a73532eedd58d63663a8adabba3d3"},
|
||||
{file = "duo_client-4.5.0-py2.py3-none-any.whl", hash = "sha256:c200f58cf4cb6f5dd7e95fae7136ea925fa61c5d5519eb16a40fea401965460d"},
|
||||
{file = "duo_client-4.5.0.tar.gz", hash = "sha256:674d1de1a46dfdd488362cd049650f0473801864b42ae458ae1803bf7e339668"},
|
||||
]
|
||||
exceptiongroup = [
|
||||
{file = "exceptiongroup-1.0.0rc9-py3-none-any.whl", hash = "sha256:2e3c3fc1538a094aab74fad52d6c33fc94de3dfee3ee01f187c0e0c72aec5337"},
|
||||
{file = "exceptiongroup-1.0.0rc9.tar.gz", hash = "sha256:9086a4a21ef9b31c72181c77c040a074ba0889ee56a7b289ff0afb0d97655f96"},
|
||||
]
|
||||
facebook-sdk = [
|
||||
{file = "facebook-sdk-3.1.0.tar.gz", hash = "sha256:cabcd2e69ea3d9f042919c99b353df7aa1e2be86d040121f6e9f5e63c1cf0f8d"},
|
||||
@ -2808,8 +2815,8 @@ kombu = [
|
||||
{file = "kombu-5.2.4.tar.gz", hash = "sha256:37cee3ee725f94ea8bb173eaab7c1760203ea53bbebae226328600f9d2799610"},
|
||||
]
|
||||
kubernetes = [
|
||||
{file = "kubernetes-24.2.0-py2.py3-none-any.whl", hash = "sha256:da19d58865cf903a8c7b9c3691a2e6315d583a98f0659964656dfdf645bf7e49"},
|
||||
{file = "kubernetes-24.2.0.tar.gz", hash = "sha256:9900f12ae92007533247167d14cdee949cd8c7721f88b4a7da5f5351da3834cd"},
|
||||
{file = "kubernetes-25.3.0-py2.py3-none-any.whl", hash = "sha256:eb42333dad0bb5caf4e66460c6a4a1a36f0f057a040f35018f6c05a699baed86"},
|
||||
{file = "kubernetes-25.3.0.tar.gz", hash = "sha256:213befbb4e5aed95f94950c7eed0c2322fc5a2f8f40932e58d28fdd42d90836c"},
|
||||
]
|
||||
lazy-object-proxy = [
|
||||
{file = "lazy-object-proxy-1.7.1.tar.gz", hash = "sha256:d609c75b986def706743cdebe5e47553f4a5a1da9c5ff66d76013ef396b5a8a4"},
|
||||
@ -3138,69 +3145,74 @@ prompt-toolkit = [
|
||||
{file = "prompt_toolkit-3.0.30.tar.gz", hash = "sha256:859b283c50bde45f5f97829f77a4674d1c1fcd88539364f1b28a37805cfd89c0"},
|
||||
]
|
||||
psycopg2-binary = [
|
||||
{file = "psycopg2-binary-2.9.4.tar.gz", hash = "sha256:a6a2d3d75d8698dee492f4af7ad07606d0734e581edf9e2ce2f74b6fce90f42e"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:e72491d72870c3cb2f0d6f4174485533caec0e9ed7e717e2859b7cc7ff2ae1c4"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2903bf90b1e6bfc9bbfc94a1db0b50ffa9830a0ca4c042fbc38d93890c02ce08"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15e0ac0ed8a85f6049e836e95ddee627766561c85be8d23f4b3edb6ddbaa7310"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:edf0a66ce9517365c7dcfed597894d8dd1f27b59e550b77a089054101435213b"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:61c6a258469c66412ae8358a0501df6ccb3bb48aa9c43b56624571ff9767f91d"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:704f1fcdc5b606b70563ea696c69bda90caee3a2f45ffc9cee60a901b394a79f"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:30200b07779446760813eef06098ec6d084131e4365b4e023eb43100de758b11"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f5fbb3b325c65010e04af206a9243e2df8606736c510c7f268aca6a93e5294a9"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:52383e932e6de5595963f9178cf2af7b9e1f3daacf5135b9c0e21aabbc5bf7c4"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0d8e0c9eec79fe1ae66691e06e3cc714da6fbd77981209bf32fa823c03dbaff8"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-win32.whl", hash = "sha256:161dc52a617f0bb610a87d391cb2e77fe65b89ebfbd752f4f3217dde701ea196"},
|
||||
{file = "psycopg2_binary-2.9.4-cp310-cp310-win_amd64.whl", hash = "sha256:33ac8b4754e6b6b21f3ee180da169d8526d91aee9408ec1fc573c16ab32b0207"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7751b11cd7f6b952b4b5ec5b93b5be9ce20faba786c18c25c354f5d8717a173c"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b216a15e13f6e763db40ac3beb74b588650bc030d10a78fde182b88d273b82b5"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eae72190be519bf2629062eab7ac8d4ceec5bd132953cefa1596584d86964fe"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:fb639a0e65dce4a9cccbcbdd8ddd0c8c6ab10bca317b827a5c52ac3c3a4ad60a"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:80ed219ce6cb21a5b53ead0edf5b56b6d23de4cb95389ac606f47670474f4816"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:f78cafa25731e0b5aa16fe20bea1abf643d4e853f6bfb8a64421b06b878e2b88"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:34fd249275faa782c3a2016e86ac2330636ac58d731a1580e7d686e3976b9536"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:24d627ed69e754c48dd142a914124858c600b4108c92546eb0ba822e63c0c6e2"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:65d5f4e70a2d3fbaa1349236968792611088f3f2dccead36c1626e1d183cc327"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-win32.whl", hash = "sha256:ae5b41dbf7731b838021923edfbe3b5ccdec84d92d5795f5229c0d08d32509d9"},
|
||||
{file = "psycopg2_binary-2.9.4-cp36-cp36m-win_amd64.whl", hash = "sha256:97e4f3d9b17d12e7c00cb1c29c0040044135cd5146838da4274615dbe0baae78"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:8660112e9127a019969a23c878e1b4a419e8a6427f9a9050c19830f152628c8a"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea8d5cd689fa7225d81ae0a049ba03e0165f4ed9ca083b19a405be9ad0b36845"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63edc507f8cbfbb5903adb75bad8a99f9798981c854df9119dbebab2ec3ee0e1"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:25e0517ad7ee3c5c3c69dbe3c1d95504c811e42f452b39a3505d0763b1f6caa0"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:0a9465f0aa36480c8e7614991cbe8ca8aa16b0517c5398a49648ce345e446c19"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:aff258af03dda9a990960a53759d10c3a9b936837c71fe2f3b581acd356b9121"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:576b9dfbcd154a0e8b5d9dae6316d037450e64a3b31df87dec71d88e2a2d5e5f"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:044b6ab68613de7ea1e63856627deea091bfea09dea5ab4f050b13250fd18cab"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7b47643c45e7619788c081d42e1d9d98c7c8a4933010a9967d097cc3c4c29f41"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-win32.whl", hash = "sha256:82df4a8600999c4c0cb7d6614df1bbdb3c74732f63e79f78487893ffbed3d083"},
|
||||
{file = "psycopg2_binary-2.9.4-cp37-cp37m-win_amd64.whl", hash = "sha256:8d7bc25729bb6d96b44f49ad78fde0e27a1a867cb205322b7e5f5b49e04d6f1f"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:2f1ded23d17af0d738e7e78087f0b88a53228887845b1989b03af4dfd3fef703"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f225784812b2b57d340f2eb0d2cebef989dcc82c288f5553e28ee9767c7c8344"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89a86c2b35460700d04b4d6461153ab39ee85af5a5385acac9563a8310e6320a"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59a3010d566a48b919490a982f6807f68842686941dc12d568e129d9cd7703d6"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:02cde837df012fa5d579b9cf4bc8e1feb460f38d61f7a4ab4a919d55a9f6eeef"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:226f11be577b70a57f4910c0ee28591d4d9fcb3d455e966267179156ae2e0c41"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:181ac372a5a5308b4076933601a9b5f0cd139b389b0aa5e164786a2abbcdb978"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d5f27b1d1b56470385faa2b2636fcb823e7ac5b5b734e0aa76b14637c66eb3b7"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:55137faec669c4277c5687c6ce7c1fbc4dece0e2f14256ee808f4a652f0a2170"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ffb2f288f577a748cc23c65a818290755a4c2da1f87a40d7055b61a096d31e20"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-win32.whl", hash = "sha256:451550e0bb5889bbabbf92575a6d6eafced941cc28c86be6ae4667f81bf32d67"},
|
||||
{file = "psycopg2_binary-2.9.4-cp38-cp38-win_amd64.whl", hash = "sha256:b23b25b1243576b952689966205ef7d4285688068b966a1ca0e620bcb390d483"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:7ad9d032dc1a31a86ca7b059f43554a049a2bfda8fe32d1492ad25f6686aff03"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7b01d07006a0ac2216921b69a220b9f0974345d0b1b36efaeabdc7550b1cc4f8"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb5341fc7c53fdd95ac2415be77b1de854ab266488cff71174ebb007baf0e675"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a431deb6ffdfa551f7400b3a94fa4b964837e67f49e3c37aa26d90dc75970816"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d6ba33f39436191ece7ea2b3d0b4dff00af71acd5c6e6f1d6b7563aa7286e9f2"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:d6c5e1df6f427d7a82606cf8f07cf3ba9fb3f366804b01e65f1f00f8df6b54f1"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:1c22c59ab7d9dc110d409445f111f58556bf699b0548f3fc5176684a29c629c4"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:b896637091cde69d170a89253dde9aee814b25ca204b7e213fd0a6462e666638"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:2535f44b00f26f6af0e949c825e6aecb9adcb56c965c17af5b97137fb69f00c0"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6a1618260a112a9c93504511f0b6254b4402a8c41b7130dc6d4c9e39aff3aa0c"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-win32.whl", hash = "sha256:e02f77b620ad6b36564fe41980865436912e21a3b1138cdde175cf24afde1bc5"},
|
||||
{file = "psycopg2_binary-2.9.4-cp39-cp39-win_amd64.whl", hash = "sha256:44f5dc9b4384bafca8429759ce76c8960ffc2b583fcad9e5dfb3e5f4894269e4"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
{file = "psycopg2-binary-2.9.5.tar.gz", hash = "sha256:33e632d0885b95a8b97165899006c40e9ecdc634a529dca7b991eb7de4ece41c"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:0775d6252ccb22b15da3b5d7adbbf8cfe284916b14b6dc0ff503a23edb01ee85"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ec46ed947801652c9643e0b1dc334cfb2781232e375ba97312c2fc256597632"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3520d7af1ebc838cc6084a3281145d5cd5bdd43fdef139e6db5af01b92596cb7"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cbc554ba47ecca8cd3396ddaca85e1ecfe3e48dd57dc5e415e59551affe568e"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:5d28ecdf191db558d0c07d0f16524ee9d67896edf2b7990eea800abeb23ebd61"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-manylinux_2_24_ppc64le.whl", hash = "sha256:b9c33d4aef08dfecbd1736ceab8b7b3c4358bf10a0121483e5cd60d3d308cc64"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:05b3d479425e047c848b9782cd7aac9c6727ce23181eb9647baf64ffdfc3da41"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:1e491e6489a6cb1d079df8eaa15957c277fdedb102b6a68cfbf40c4994412fd0"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:9e32cedc389bcb76d9f24ea8a012b3cb8385ee362ea437e1d012ffaed106c17d"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:46850a640df62ae940e34a163f72e26aca1f88e2da79148e1862faaac985c302"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-win32.whl", hash = "sha256:3d790f84201c3698d1bfb404c917f36e40531577a6dda02e45ba29b64d539867"},
|
||||
{file = "psycopg2_binary-2.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:1764546ffeaed4f9428707be61d68972eb5ede81239b46a45843e0071104d0dd"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7cf1d44e710ca3a9ce952bda2855830fe9f9017ed6259e01fcd71ea6287565f5"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:024030b13bdcbd53d8a93891a2cf07719715724fc9fee40243f3bd78b4264b8f"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcda1c84a1c533c528356da5490d464a139b6e84eb77cc0b432e38c5c6dd7882"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:2ef892cabdccefe577088a79580301f09f2a713eb239f4f9f62b2b29cafb0577"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-manylinux_2_24_ppc64le.whl", hash = "sha256:af0516e1711995cb08dc19bbd05bec7dbdebf4185f68870595156718d237df3e"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e72c91bda9880f097c8aa3601a2c0de6c708763ba8128006151f496ca9065935"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e67b3c26e9b6d37b370c83aa790bbc121775c57bfb096c2e77eacca25fd0233b"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:5fc447058d083b8c6ac076fc26b446d44f0145308465d745fba93a28c14c9e32"},
|
||||
{file = "psycopg2_binary-2.9.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d892bfa1d023c3781a3cab8dd5af76b626c483484d782e8bd047c180db590e4c"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-macosx_10_14_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:6e63814ec71db9bdb42905c925639f319c80e7909fb76c3b84edc79dadef8d60"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:212757ffcecb3e1a5338d4e6761bf9c04f750e7d027117e74aa3cd8a75bb6fbd"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f8a9bcab7b6db2e3dbf65b214dfc795b4c6b3bb3af922901b6a67f7cb47d5f8"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_aarch64.whl", hash = "sha256:56b2957a145f816726b109ee3d4e6822c23f919a7d91af5a94593723ed667835"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-manylinux_2_24_ppc64le.whl", hash = "sha256:f95b8aca2703d6a30249f83f4fe6a9abf2e627aa892a5caaab2267d56be7ab69"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:70831e03bd53702c941da1a1ad36c17d825a24fbb26857b40913d58df82ec18b"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:dbc332beaf8492b5731229a881807cd7b91b50dbbbaf7fe2faf46942eda64a24"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:2d964eb24c8b021623df1c93c626671420c6efadbdb8655cb2bd5e0c6fa422ba"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:95076399ec3b27a8f7fa1cc9a83417b1c920d55cf7a97f718a94efbb96c7f503"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-win32.whl", hash = "sha256:3fc33295cfccad697a97a76dec3f1e94ad848b7b163c3228c1636977966b51e2"},
|
||||
{file = "psycopg2_binary-2.9.5-cp36-cp36m-win_amd64.whl", hash = "sha256:02551647542f2bf89073d129c73c05a25c372fc0a49aa50e0de65c3c143d8bd0"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:63e318dbe52709ed10d516a356f22a635e07a2e34c68145484ed96a19b0c4c68"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7e518a0911c50f60313cb9e74a169a65b5d293770db4770ebf004245f24b5c5"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9d38a4656e4e715d637abdf7296e98d6267df0cc0a8e9a016f8ba07e4aa3eeb"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_aarch64.whl", hash = "sha256:68d81a2fe184030aa0c5c11e518292e15d342a667184d91e30644c9d533e53e1"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-manylinux_2_24_ppc64le.whl", hash = "sha256:7ee3095d02d6f38bd7d9a5358fcc9ea78fcdb7176921528dd709cc63f40184f5"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:46512486be6fbceef51d7660dec017394ba3e170299d1dc30928cbedebbf103a"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b911dfb727e247340d36ae20c4b9259e4a64013ab9888ccb3cbba69b77fd9636"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:422e3d43b47ac20141bc84b3d342eead8d8099a62881a501e97d15f6addabfe9"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c5682a45df7d9642eff590abc73157c887a68f016df0a8ad722dcc0f888f56d7"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-win32.whl", hash = "sha256:b8104f709590fff72af801e916817560dbe1698028cd0afe5a52d75ceb1fce5f"},
|
||||
{file = "psycopg2_binary-2.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:7b3751857da3e224f5629400736a7b11e940b5da5f95fa631d86219a1beaafec"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:043a9fd45a03858ff72364b4b75090679bd875ee44df9c0613dc862ca6b98460"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9ffdc51001136b699f9563b1c74cc1f8c07f66ef7219beb6417a4c8aaa896c28"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c15ba5982c177bc4b23a7940c7e4394197e2d6a424a2d282e7c236b66da6d896"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc85b3777068ed30aff8242be2813038a929f2084f69e43ef869daddae50f6ee"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_aarch64.whl", hash = "sha256:215d6bf7e66732a514f47614f828d8c0aaac9a648c46a831955cb103473c7147"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-manylinux_2_24_ppc64le.whl", hash = "sha256:7d07f552d1e412f4b4e64ce386d4c777a41da3b33f7098b6219012ba534fb2c2"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a0adef094c49f242122bb145c3c8af442070dc0e4312db17e49058c1702606d4"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:00475004e5ed3e3bf5e056d66e5dcdf41a0dc62efcd57997acd9135c40a08a50"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:7d88db096fa19d94f433420eaaf9f3c45382da2dd014b93e4bf3215639047c16"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:902844f9c4fb19b17dfa84d9e2ca053d4a4ba265723d62ea5c9c26b38e0aa1e6"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-win32.whl", hash = "sha256:4e7904d1920c0c89105c0517dc7e3f5c20fb4e56ba9cdef13048db76947f1d79"},
|
||||
{file = "psycopg2_binary-2.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:a36a0e791805aa136e9cbd0ffa040d09adec8610453ee8a753f23481a0057af5"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_10_15_x86_64.macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:25382c7d174c679ce6927c16b6fbb68b10e56ee44b1acb40671e02d29f2fce7c"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9c38d3869238e9d3409239bc05bc27d6b7c99c2a460ea337d2814b35fb4fea1b"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5c6527c8efa5226a9e787507652dd5ba97b62d29b53c371a85cd13f957fe4d42"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e59137cdb970249ae60be2a49774c6dfb015bd0403f05af1fe61862e9626642d"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:d4c7b3a31502184e856df1f7bbb2c3735a05a8ce0ade34c5277e1577738a5c91"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-manylinux_2_24_ppc64le.whl", hash = "sha256:b9a794cef1d9c1772b94a72eec6da144c18e18041d294a9ab47669bc77a80c1d"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5254cbd4f4855e11cebf678c1a848a3042d455a22a4ce61349c36aafd4c2267"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c5e65c6ac0ae4bf5bef1667029f81010b6017795dcb817ba5c7b8a8d61fab76f"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:74eddec4537ab1f701a1647214734bc52cee2794df748f6ae5908e00771f180a"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:01ad49d68dd8c5362e4bfb4158f2896dc6e0c02e87b8a3770fc003459f1a4425"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-win32.whl", hash = "sha256:937880290775033a743f4836aa253087b85e62784b63fd099ee725d567a48aa1"},
|
||||
{file = "psycopg2_binary-2.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:484405b883630f3e74ed32041a87456c5e0e63a8e3429aa93e8714c366d62bd1"},
|
||||
]
|
||||
pyasn1 = [
|
||||
{file = "pyasn1-0.4.8-py2.py3-none-any.whl", hash = "sha256:39c7e2ec30515947ff4e87fb6f456dfc6e84857d34be479c9d4a4ba4bf46aa5d"},
|
||||
@ -3292,8 +3304,8 @@ pyjwt = [
|
||||
{file = "PyJWT-2.6.0.tar.gz", hash = "sha256:69285c7e31fc44f68a1feb309e948e0df53259d579295e6cfe2b1792329f05fd"},
|
||||
]
|
||||
pylint = [
|
||||
{file = "pylint-2.15.4-py3-none-any.whl", hash = "sha256:629cf1dbdfb6609d7e7a45815a8bb59300e34aa35783b5ac563acaca2c4022e9"},
|
||||
{file = "pylint-2.15.4.tar.gz", hash = "sha256:5441e9294335d354b7bad57c1044e5bd7cce25c433475d76b440e53452fa5cb8"},
|
||||
{file = "pylint-2.15.5-py3-none-any.whl", hash = "sha256:c2108037eb074334d9e874dc3c783752cc03d0796c88c9a9af282d0f161a1004"},
|
||||
{file = "pylint-2.15.5.tar.gz", hash = "sha256:3b120505e5af1d06a5ad76b55d8660d44bf0f2fc3c59c2bdd94e39188ee3a4df"},
|
||||
]
|
||||
pylint-django = [
|
||||
{file = "pylint-django-2.5.3.tar.gz", hash = "sha256:0ac090d106c62fe33782a1d01bda1610b761bb1c9bf5035ced9d5f23a13d8591"},
|
||||
@ -3352,8 +3364,8 @@ pysocks = [
|
||||
{file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"},
|
||||
{file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"},
|
||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
||||
]
|
||||
pytest-django = [
|
||||
{file = "pytest-django-4.5.2.tar.gz", hash = "sha256:d9076f759bb7c36939dbdd5ae6633c18edfc2902d1a69fdbefd2426b970ce6c2"},
|
||||
@ -3457,8 +3469,8 @@ selenium = [
|
||||
{file = "selenium-4.5.0-py3-none-any.whl", hash = "sha256:a733dd77d3171b846893f4d51b18967d809313f547a10974e26579f9ce797462"},
|
||||
]
|
||||
sentry-sdk = [
|
||||
{file = "sentry-sdk-1.10.0.tar.gz", hash = "sha256:1b965bcdbfe52321bb1307c7c93c74035afdbfceb5f585f01a963327c5befc4e"},
|
||||
{file = "sentry_sdk-1.10.0-py2.py3-none-any.whl", hash = "sha256:8c648e96e0e2ec5e17ca75a28c442e2f523453fa7cf761ec093f4a656153490e"},
|
||||
{file = "sentry-sdk-1.10.1.tar.gz", hash = "sha256:105faf7bd7b7fa25653404619ee261527266b14103fe1389e0ce077bd23a9691"},
|
||||
{file = "sentry_sdk-1.10.1-py2.py3-none-any.whl", hash = "sha256:06c0fa9ccfdc80d7e3b5d2021978d6eb9351fa49db9b5847cf4d1f2a473414ad"},
|
||||
]
|
||||
service-identity = [
|
||||
{file = "service-identity-21.1.0.tar.gz", hash = "sha256:6e6c6086ca271dc11b033d17c3a8bea9f24ebff920c587da090afc9519419d34"},
|
||||
|
@ -100,7 +100,7 @@ addopts = "-p no:celery --junitxml=unittest.xml"
|
||||
|
||||
[tool.poetry]
|
||||
name = "authentik"
|
||||
version = "2022.10.0"
|
||||
version = "2022.10.4"
|
||||
description = ""
|
||||
authors = ["authentik Team <hello@goauthentik.io>"]
|
||||
|
||||
|
64
schema.yml
64
schema.yml
@ -1,7 +1,7 @@
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: authentik
|
||||
version: 2022.10.0
|
||||
version: 2022.10.4
|
||||
description: Making authentication simple.
|
||||
contact:
|
||||
email: hello@goauthentik.io
|
||||
@ -22277,6 +22277,10 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
- in: query
|
||||
name: flow__slug
|
||||
schema:
|
||||
type: string
|
||||
- in: query
|
||||
name: name
|
||||
schema:
|
||||
@ -24520,6 +24524,10 @@ paths:
|
||||
operationId: stages_user_write_list
|
||||
description: UserWriteStage Viewset
|
||||
parameters:
|
||||
- in: query
|
||||
name: can_create_users
|
||||
schema:
|
||||
type: boolean
|
||||
- in: query
|
||||
name: create_users_as_inactive
|
||||
schema:
|
||||
@ -25215,6 +25223,13 @@ components:
|
||||
- last_used
|
||||
- user
|
||||
- user_agent
|
||||
AuthenticationEnum:
|
||||
enum:
|
||||
- none
|
||||
- require_authenticated
|
||||
- require_unauthenticated
|
||||
- require_superuser
|
||||
type: string
|
||||
AuthenticatorAttachmentEnum:
|
||||
enum:
|
||||
- platform
|
||||
@ -27512,6 +27527,11 @@ components:
|
||||
- $ref: '#/components/schemas/DeniedActionEnum'
|
||||
description: Configure what should happen when a flow denies access to a
|
||||
user.
|
||||
authentication:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticationEnum'
|
||||
description: Required level of authentication and authorization to access
|
||||
a flow.
|
||||
required:
|
||||
- background
|
||||
- cache_count
|
||||
@ -27804,6 +27824,11 @@ components:
|
||||
- $ref: '#/components/schemas/DeniedActionEnum'
|
||||
description: Configure what should happen when a flow denies access to a
|
||||
user.
|
||||
authentication:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticationEnum'
|
||||
description: Required level of authentication and authorization to access
|
||||
a flow.
|
||||
required:
|
||||
- designation
|
||||
- name
|
||||
@ -28370,8 +28395,18 @@ components:
|
||||
single_use:
|
||||
type: boolean
|
||||
description: When enabled, the invitation will be deleted after usage.
|
||||
flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, only the configured flow can use this invitation.
|
||||
flow_obj:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/Flow'
|
||||
readOnly: true
|
||||
required:
|
||||
- created_by
|
||||
- flow_obj
|
||||
- name
|
||||
- pk
|
||||
InvitationRequest:
|
||||
@ -28392,6 +28427,11 @@ components:
|
||||
single_use:
|
||||
type: boolean
|
||||
description: When enabled, the invitation will be deleted after usage.
|
||||
flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, only the configured flow can use this invitation.
|
||||
required:
|
||||
- name
|
||||
InvitationStage:
|
||||
@ -33535,6 +33575,11 @@ components:
|
||||
- $ref: '#/components/schemas/DeniedActionEnum'
|
||||
description: Configure what should happen when a flow denies access to a
|
||||
user.
|
||||
authentication:
|
||||
allOf:
|
||||
- $ref: '#/components/schemas/AuthenticationEnum'
|
||||
description: Required level of authentication and authorization to access
|
||||
a flow.
|
||||
PatchedFlowStageBindingRequest:
|
||||
type: object
|
||||
description: FlowStageBinding Serializer
|
||||
@ -33682,6 +33727,11 @@ components:
|
||||
single_use:
|
||||
type: boolean
|
||||
description: When enabled, the invitation will be deleted after usage.
|
||||
flow:
|
||||
type: string
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: When set, only the configured flow can use this invitation.
|
||||
PatchedInvitationStageRequest:
|
||||
type: object
|
||||
description: InvitationStage Serializer
|
||||
@ -34885,6 +34935,10 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Optionally add newly created users to this group.
|
||||
can_create_users:
|
||||
type: boolean
|
||||
description: When set, this stage can create users. If not enabled and no
|
||||
user is available, stage will fail.
|
||||
user_path_template:
|
||||
type: string
|
||||
PatchedWebAuthnDeviceRequest:
|
||||
@ -38023,6 +38077,10 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Optionally add newly created users to this group.
|
||||
can_create_users:
|
||||
type: boolean
|
||||
description: When set, this stage can create users. If not enabled and no
|
||||
user is available, stage will fail.
|
||||
user_path_template:
|
||||
type: string
|
||||
required:
|
||||
@ -38051,6 +38109,10 @@ components:
|
||||
format: uuid
|
||||
nullable: true
|
||||
description: Optionally add newly created users to this group.
|
||||
can_create_users:
|
||||
type: boolean
|
||||
description: When set, this stage can create users. If not enabled and no
|
||||
user is available, stage will fail.
|
||||
user_path_template:
|
||||
type: string
|
||||
required:
|
||||
|
704
web/package-lock.json
generated
704
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -53,18 +53,18 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.19.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.19.6",
|
||||
"@babel/plugin-proposal-decorators": "^7.20.0",
|
||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||
"@babel/preset-env": "^7.19.4",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@codemirror/lang-html": "^6.1.2",
|
||||
"@codemirror/lang-javascript": "^6.1.0",
|
||||
"@codemirror/lang-python": "^6.0.3",
|
||||
"@codemirror/lang-xml": "^6.0.0",
|
||||
"@codemirror/legacy-modes": "^6.1.0",
|
||||
"@codemirror/lang-html": "^6.1.3",
|
||||
"@codemirror/lang-javascript": "^6.1.1",
|
||||
"@codemirror/lang-python": "^6.0.4",
|
||||
"@codemirror/lang-xml": "^6.0.1",
|
||||
"@codemirror/legacy-modes": "^6.2.0",
|
||||
"@formatjs/intl-listformat": "^7.1.3",
|
||||
"@fortawesome/fontawesome-free": "^6.2.0",
|
||||
"@goauthentik/api": "^2022.9.0-1666252592",
|
||||
"@goauthentik/api": "^2022.11.3-1671801250",
|
||||
"@jackfranklin/rollup-plugin-markdown": "^0.4.0",
|
||||
"@lingui/cli": "^3.14.0",
|
||||
"@lingui/core": "^3.14.0",
|
||||
@ -73,21 +73,21 @@
|
||||
"@patternfly/patternfly": "^4.217.1",
|
||||
"@polymer/iron-form": "^3.0.1",
|
||||
"@polymer/paper-input": "^3.2.1",
|
||||
"@rollup/plugin-babel": "^6.0.0",
|
||||
"@rollup/plugin-commonjs": "^23.0.1",
|
||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||
"@rollup/plugin-replace": "^5.0.0",
|
||||
"@rollup/plugin-typescript": "^9.0.1",
|
||||
"@sentry/browser": "^7.16.0",
|
||||
"@sentry/tracing": "^7.16.0",
|
||||
"@rollup/plugin-babel": "^6.0.2",
|
||||
"@rollup/plugin-commonjs": "^23.0.2",
|
||||
"@rollup/plugin-node-resolve": "^15.0.1",
|
||||
"@rollup/plugin-replace": "^5.0.1",
|
||||
"@rollup/plugin-typescript": "^9.0.2",
|
||||
"@sentry/browser": "^7.17.2",
|
||||
"@sentry/tracing": "^7.17.1",
|
||||
"@squoosh/cli": "^0.7.2",
|
||||
"@trivago/prettier-plugin-sort-imports": "^3.4.0",
|
||||
"@types/chart.js": "^2.9.37",
|
||||
"@types/codemirror": "5.60.5",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
"@types/mermaid": "^9.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.41.0",
|
||||
"@typescript-eslint/parser": "^5.41.0",
|
||||
"@webcomponents/webcomponentsjs": "^2.7.0",
|
||||
"babel-plugin-macros": "^3.1.0",
|
||||
"babel-plugin-tsconfig-paths": "^1.0.3",
|
||||
@ -97,7 +97,7 @@
|
||||
"codemirror": "^6.0.1",
|
||||
"construct-style-sheets-polyfill": "^3.1.0",
|
||||
"country-flag-icons": "^1.5.5",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-plugin-custom-elements": "0.0.6",
|
||||
"eslint-plugin-lit": "^1.6.1",
|
||||
@ -106,7 +106,7 @@
|
||||
"mermaid": "^9.1.7",
|
||||
"moment": "^2.29.4",
|
||||
"prettier": "^2.7.1",
|
||||
"pyright": "^1.1.276",
|
||||
"pyright": "^1.1.277",
|
||||
"rapidoc": "^9.3.3",
|
||||
"rollup": "^2.79.1",
|
||||
"rollup-plugin-copy": "^3.4.0",
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { DesignationToLabel, LayoutToLabel } from "@goauthentik/admin/flows/utils";
|
||||
import { AuthenticationEnum } from "@goauthentik/api/dist/models/AuthenticationEnum";
|
||||
import { DEFAULT_CONFIG, config } from "@goauthentik/common/api/config";
|
||||
import { first } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
@ -141,6 +142,37 @@ export class FlowForm extends ModelForm<Flow, string> {
|
||||
</option>`;
|
||||
}
|
||||
|
||||
renderAuthentication(): TemplateResult {
|
||||
return html`
|
||||
<option
|
||||
value=${AuthenticationEnum.None}
|
||||
?selected=${this.instance?.authentication === AuthenticationEnum.None}
|
||||
>
|
||||
${t`No requirement`}
|
||||
</option>
|
||||
<option
|
||||
value=${AuthenticationEnum.RequireAuthenticated}
|
||||
?selected=${this.instance?.authentication ===
|
||||
AuthenticationEnum.RequireAuthenticated}
|
||||
>
|
||||
${t`Require authentication`}
|
||||
</option>
|
||||
<option
|
||||
value=${AuthenticationEnum.RequireUnauthenticated}
|
||||
?selected=${this.instance?.authentication ===
|
||||
AuthenticationEnum.RequireUnauthenticated}
|
||||
>
|
||||
${t`Require no authentication.`}
|
||||
</option>
|
||||
<option
|
||||
value=${AuthenticationEnum.RequireSuperuser}
|
||||
?selected=${this.instance?.authentication === AuthenticationEnum.RequireSuperuser}
|
||||
>
|
||||
${t`Require superuser.`}
|
||||
</option>
|
||||
`;
|
||||
}
|
||||
|
||||
renderLayout(): TemplateResult {
|
||||
return html`
|
||||
<option
|
||||
@ -224,6 +256,18 @@ export class FlowForm extends ModelForm<Flow, string> {
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Authentication`}
|
||||
?required=${true}
|
||||
name="authentication"
|
||||
>
|
||||
<select class="pf-c-form-control">
|
||||
${this.renderAuthentication()}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`Required authentication level for this flow.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Designation`}
|
||||
?required=${true}
|
||||
|
@ -9,8 +9,15 @@ import { t } from "@lingui/macro";
|
||||
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import { until } from "lit/directives/until.js";
|
||||
|
||||
import { Invitation, StagesApi } from "@goauthentik/api";
|
||||
import {
|
||||
FlowsApi,
|
||||
FlowsInstancesListDesignationEnum,
|
||||
Invitation,
|
||||
StagesApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-invitation-form")
|
||||
export class InvitationForm extends ModelForm<Invitation, string> {
|
||||
@ -66,6 +73,34 @@ export class InvitationForm extends ModelForm<Invitation, string> {
|
||||
value="${dateTimeLocal(first(this.instance?.expires, new Date()))}"
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Flow`} ?required=${true} name="flow">
|
||||
<select class="pf-c-form-control">
|
||||
<option value="" ?selected=${this.instance?.flow === undefined}>
|
||||
---------
|
||||
</option>
|
||||
${until(
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesList({
|
||||
ordering: "slug",
|
||||
designation: FlowsInstancesListDesignationEnum.Enrollment,
|
||||
})
|
||||
.then((flows) => {
|
||||
return flows.results.map((flow) => {
|
||||
return html`<option
|
||||
value=${ifDefined(flow.pk)}
|
||||
?selected=${this.instance?.flow === flow.pk}
|
||||
>
|
||||
${flow.name} (${flow.slug})
|
||||
</option>`;
|
||||
});
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`When selected, the invite will only be usable with the flow. By default the invite is accepted on all flows with invitation stages.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${t`Attributes`} name="fixedData">
|
||||
<ak-codemirror
|
||||
mode="yaml"
|
||||
|
@ -14,12 +14,12 @@ import PFFormControl from "@patternfly/patternfly/components/FormControl/form-co
|
||||
import PFFlex from "@patternfly/patternfly/layouts/Flex/flex.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { StagesApi } from "@goauthentik/api";
|
||||
import { Invitation, StagesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-invitation-list-link")
|
||||
export class InvitationListLink extends AKElement {
|
||||
@property()
|
||||
invitation?: string;
|
||||
@property({ attribute: false })
|
||||
invitation?: Invitation;
|
||||
|
||||
@property()
|
||||
selectedFlow?: string;
|
||||
@ -29,60 +29,67 @@ export class InvitationListLink extends AKElement {
|
||||
}
|
||||
|
||||
renderLink(): string {
|
||||
return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?itoken=${this.invitation}`;
|
||||
if (this.invitation?.flowObj) {
|
||||
this.selectedFlow = this.invitation.flowObj?.slug;
|
||||
}
|
||||
return `${window.location.protocol}//${window.location.host}/if/flow/${this.selectedFlow}/?itoken=${this.invitation?.pk}`;
|
||||
}
|
||||
|
||||
renderFlowSelector(): TemplateResult {
|
||||
return html`<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<select
|
||||
class="pf-c-form-control"
|
||||
@change=${(ev: Event) => {
|
||||
const current = (ev.target as HTMLInputElement).value;
|
||||
this.selectedFlow = current;
|
||||
}}
|
||||
>
|
||||
${until(
|
||||
new StagesApi(DEFAULT_CONFIG)
|
||||
.stagesInvitationStagesList({
|
||||
ordering: "name",
|
||||
noFlows: false,
|
||||
})
|
||||
.then((stages) => {
|
||||
if (
|
||||
!this.selectedFlow &&
|
||||
stages.results.length > 0 &&
|
||||
stages.results[0].flowSet
|
||||
) {
|
||||
this.selectedFlow = stages.results[0].flowSet[0].slug;
|
||||
}
|
||||
const seenFlowSlugs: string[] = [];
|
||||
return stages.results.map((stage) => {
|
||||
return stage.flowSet?.map((flow) => {
|
||||
if (seenFlowSlugs.includes(flow.slug)) {
|
||||
return html``;
|
||||
}
|
||||
seenFlowSlugs.push(flow.slug);
|
||||
return html`<option
|
||||
value=${flow.slug}
|
||||
?selected=${flow.slug === this.selectedFlow}
|
||||
>
|
||||
${flow.slug}
|
||||
</option>`;
|
||||
});
|
||||
});
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</dd>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${t`Select an enrollment flow`}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<select
|
||||
class="pf-c-form-control"
|
||||
@change=${(ev: Event) => {
|
||||
const current = (ev.target as HTMLInputElement).value;
|
||||
this.selectedFlow = current;
|
||||
}}
|
||||
>
|
||||
${until(
|
||||
new StagesApi(DEFAULT_CONFIG)
|
||||
.stagesInvitationStagesList({
|
||||
ordering: "name",
|
||||
noFlows: false,
|
||||
})
|
||||
.then((stages) => {
|
||||
if (
|
||||
!this.selectedFlow &&
|
||||
stages.results.length > 0 &&
|
||||
stages.results[0].flowSet
|
||||
) {
|
||||
this.selectedFlow = stages.results[0].flowSet[0].slug;
|
||||
}
|
||||
const seenFlowSlugs: string[] = [];
|
||||
return stages.results.map((stage) => {
|
||||
return stage.flowSet?.map((flow) => {
|
||||
if (seenFlowSlugs.includes(flow.slug)) {
|
||||
return html``;
|
||||
}
|
||||
seenFlowSlugs.push(flow.slug);
|
||||
return html`<option
|
||||
value=${flow.slug}
|
||||
?selected=${flow.slug === this.selectedFlow}
|
||||
>
|
||||
${flow.slug}
|
||||
</option>`;
|
||||
});
|
||||
});
|
||||
}),
|
||||
html`<option>${t`Loading...`}</option>`,
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
${this.invitation?.flow === undefined ? this.renderFlowSelector() : html``}
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
|
@ -2,6 +2,7 @@ import "@goauthentik/admin/stages/invitation/InvitationForm";
|
||||
import "@goauthentik/admin/stages/invitation/InvitationListLink";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import { PFColor } from "@goauthentik/elements/Label";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
@ -18,7 +19,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
|
||||
import { Invitation, StagesApi } from "@goauthentik/api";
|
||||
import { FlowDesignationEnum, Invitation, StagesApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-stage-invitation-list")
|
||||
export class InvitationListPage extends TablePage<Invitation> {
|
||||
@ -49,12 +50,24 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
@state()
|
||||
invitationStageExists = false;
|
||||
|
||||
@state()
|
||||
multipleEnrollmentFlows = false;
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Invitation>> {
|
||||
// Check if any invitation stages exist
|
||||
const stages = await new StagesApi(DEFAULT_CONFIG).stagesInvitationStagesList({
|
||||
noFlows: false,
|
||||
});
|
||||
this.invitationStageExists = stages.pagination.count > 0;
|
||||
this.expandable = this.invitationStageExists;
|
||||
stages.results.forEach((stage) => {
|
||||
const enrollmentFlows = (stage.flowSet || []).filter(
|
||||
(flow) => flow.designation === FlowDesignationEnum.Enrollment,
|
||||
);
|
||||
if (enrollmentFlows.length > 1) {
|
||||
this.multipleEnrollmentFlows = true;
|
||||
}
|
||||
});
|
||||
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
@ -96,7 +109,14 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
|
||||
row(item: Invitation): TemplateResult[] {
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`<div>${item.name}</div>
|
||||
${!item.flowObj && this.multipleEnrollmentFlows
|
||||
? html`
|
||||
<ak-label color=${PFColor.Orange}>
|
||||
${t`Invitation not limited to any flow, and can be used with any enrollment flow.`}
|
||||
</ak-label>
|
||||
`
|
||||
: html``}`,
|
||||
html`${item.createdBy?.username}`,
|
||||
html`${item.expires?.toLocaleString() || t`-`}`,
|
||||
html` <ak-forms-modal>
|
||||
@ -114,7 +134,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||
return html` <td role="cell" colspan="3">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<ak-stage-invitation-list-link
|
||||
invitation=${item.pk}
|
||||
.invitation=${item}
|
||||
></ak-stage-invitation-list-link>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -59,6 +59,21 @@ export class UserWriteStageForm extends ModelForm<UserWriteStage, string> {
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header"> ${t`Stage-specific settings`} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal name="canCreateUsers">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="pf-c-check__input"
|
||||
?checked=${first(this.instance?.canCreateUsers, false)}
|
||||
/>
|
||||
<label class="pf-c-check__label">
|
||||
${t`Can create users`}
|
||||
</label>
|
||||
</div>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${t`When enabled, this stage has the ability to create new users. If no user is available in the flow with this disabled, the stage will fail.`}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="createUsersAsInactive">
|
||||
<div class="pf-c-check">
|
||||
<input
|
||||
|
@ -3,7 +3,7 @@ export const SUCCESS_CLASS = "pf-m-success";
|
||||
export const ERROR_CLASS = "pf-m-danger";
|
||||
export const PROGRESS_CLASS = "pf-m-in-progress";
|
||||
export const CURRENT_CLASS = "pf-m-current";
|
||||
export const VERSION = "2022.10.0";
|
||||
export const VERSION = "2022.10.4";
|
||||
export const TITLE_DEFAULT = "authentik";
|
||||
export const ROUTE_SEPARATOR = ";";
|
||||
|
||||
|
@ -80,5 +80,5 @@ export function currentInterface(): string {
|
||||
if (pathMatches && pathMatches.length >= 2) {
|
||||
currentInterface = pathMatches[1];
|
||||
}
|
||||
return currentInterface;
|
||||
return currentInterface.toLowerCase();
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { currentInterface } from "@goauthentik/common/sentry";
|
||||
import { me } from "@goauthentik/common/users";
|
||||
|
||||
import { UserSelf } from "@goauthentik/api";
|
||||
@ -65,6 +66,12 @@ export class DefaultUIConfig implements UIConfig {
|
||||
perPage: 20,
|
||||
};
|
||||
locale = "";
|
||||
|
||||
constructor() {
|
||||
if (currentInterface() === "user") {
|
||||
this.enabledFeatures.apiDrawer = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let globalUiConfig: Promise<UIConfig>;
|
||||
|
@ -98,13 +98,6 @@ export class UserSettingsFlowExecutor extends AKElement implements StageHost {
|
||||
if (!this.flowSlug) {
|
||||
return;
|
||||
}
|
||||
new FlowsApi(DEFAULT_CONFIG)
|
||||
.flowsInstancesExecuteRetrieve({
|
||||
slug: this.flowSlug || "",
|
||||
})
|
||||
.then(() => {
|
||||
this.nextChallenge();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -5,3 +5,5 @@ title: TOTP authenticator setup stage
|
||||
This stage configures a time-based OTP Device, such as Google Authenticator or Authy.
|
||||
|
||||
You can configure how many digits should be used for the OTP Token.
|
||||
|
||||
The Config URL's Issuer is set based on the currently active tenant's branding title. The default setup can cause issues if the same username is used on multiple authentik issues within the same authenticator app, so changing the tenant tile is recommended.
|
||||
|
@ -25,8 +25,48 @@ return True
|
||||
|
||||
You can also use custom email templates, to use your own design or layout.
|
||||
|
||||
import Tabs from "@theme/Tabs";
|
||||
import TabItem from "@theme/TabItem";
|
||||
|
||||
<Tabs
|
||||
defaultValue="docker-compose"
|
||||
values={[
|
||||
{label: 'docker-compose', value: 'docker-compose'},
|
||||
{label: 'Kubernetes', value: 'kubernetes'},
|
||||
]}>
|
||||
<TabItem value="docker-compose">
|
||||
Place any custom templates in the `custom-templates` Folder, which is in the same folder as your docker-compose file. Afterwards, you'll be able to select the template when creating/editing an Email stage.
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="kubernetes">
|
||||
Create a ConfigMap with your email templates:
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: authentik-templates
|
||||
namespace: authentik
|
||||
data:
|
||||
my-template.html: |
|
||||
<tr>...
|
||||
```
|
||||
|
||||
Then, in the helm chart add this to your `values.yaml` file:
|
||||
|
||||
```yaml
|
||||
volumes:
|
||||
- name: email-templates
|
||||
configMap:
|
||||
name: authentik-templates
|
||||
volumeMounts:
|
||||
- name: email-templates
|
||||
mountPath: /templates
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::info
|
||||
This is currently only supported for docker-compose installs, and supported starting version 0.15.
|
||||
:::
|
@ -3789,6 +3789,19 @@ Changed response : **200 OK**
|
||||
- web/flows: update flow background
|
||||
- website/docs: add warning to trace log level
|
||||
|
||||
## Fixed in 2022.10.1
|
||||
|
||||
- blueprints: fix error when cleaning up unset attribute
|
||||
- blueprints: improve error handling
|
||||
- core: check if session is authenticated before showing linked message
|
||||
- core: explicitly enable locales (#3889)
|
||||
- core: refactor MessageStage to not use dynamic class
|
||||
- core: set prehydrated locale based on active backend locale
|
||||
- core: show success message when authenticating/enrolling after flow is finished
|
||||
- flows: fix error due to not validating error challenge
|
||||
- sources/saml: set username field to name_id attribute
|
||||
- web/common: disable API Drawer by default in user interface
|
||||
|
||||
## Upgrading
|
||||
|
||||
This release does not introduce any new requirements.
|
||||
|
29
website/docs/security/CVE-2022-23555.md
Normal file
29
website/docs/security/CVE-2022-23555.md
Normal file
@ -0,0 +1,29 @@
|
||||
# CVE-2022-23555
|
||||
|
||||
## Token reuse in invitation URLs leads to access control bypass via the use of a different enrollment flow
|
||||
|
||||
### Summary
|
||||
|
||||
Token reuse in invitation URLs leads to access control bypass via the use of a different enrollment flow than in the one provided.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2022.11.4, 2022.10.4 and 2022.12.0 fix this issue, for other versions the workaround can be used.
|
||||
|
||||
### Impact
|
||||
|
||||
Only configurations using both invitations and have multiple enrollment flows with invitation stages that grant different permissions are affected. The default configuration is not vulnerable, and neither are configurations with a single enrollment flow.
|
||||
|
||||
### Details
|
||||
|
||||
The vulnerability allows an attacker that knows different invitation flows names (e.g. `enrollment-invitation-test` and `enrollment-invitation-admin`) via either different invite links or via brute forcing to signup via a single invitation url for any valid invite link received (it can even be a url for a third flow as long as it's a valid invite) as the token used in the `Invitations` section of the Admin interface does NOT change when a different `enrollment flow` is selected via the interface and it is NOT bound to the selected flow, so it will be valid for any flow when used.
|
||||
|
||||
### Workarounds
|
||||
|
||||
As a workaround, fixed data can be added to invitations which can be checked in the flow to deny requests. Alternatively, an identifier with high entropy (like a UUID) can be used as flow slug, mitigating the attack vector by exponentially decreasing the possibility of discovering other flows.
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
19
website/docs/security/CVE-2022-46145.md
Normal file
19
website/docs/security/CVE-2022-46145.md
Normal file
@ -0,0 +1,19 @@
|
||||
# CVE-2022-46145
|
||||
|
||||
## Unauthorized user creation and potential account takeover
|
||||
|
||||
### Impact
|
||||
|
||||
With the default flows, unauthenticated users can create new accounts in authentik. If a flow exists that allows for email-verified password recovery, this can be used to overwrite the email address of admin accounts and take over their accounts
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2022.11.2 and 2022.10.2 fix this issue, for other versions the workaround can be used.
|
||||
|
||||
### Workarounds
|
||||
|
||||
A policy can be created and bound to the `default-user-settings-flow` flow with the following contents
|
||||
|
||||
```python
|
||||
return request.user.is_authenticated
|
||||
```
|
25
website/docs/security/CVE-2022-46172.md
Normal file
25
website/docs/security/CVE-2022-46172.md
Normal file
@ -0,0 +1,25 @@
|
||||
# CVE-2022-46172
|
||||
|
||||
## Existing Authenticated Users can Create Arbitrary Accounts
|
||||
|
||||
### Summary
|
||||
|
||||
Any authenticated user can create an arbitrary number of accounts through the default flows. This would circumvent any policy in a situation where it is undesirable for users to create new accounts by themselves. This may also have carry over consequences to other applications being how these new basic accounts would exist throughout the SSO infrastructure. By default the newly created accounts cannot be logged into as no password reset exists by default. However password resets are likely to be enabled by most installations.
|
||||
|
||||
### Patches
|
||||
|
||||
authentik 2022.11.4, 2022.10.4 and 2022.12.0 fix this issue.
|
||||
|
||||
### Impact
|
||||
|
||||
This vulnerability could make it much easier for name and email collisions to occur, making it harder for user to log in. This also makes it more difficult for admins to properly administer users since more and more confusing users will exist. This paired with password reset flows if enabled would mean a circumvention of on-boarding policies. Say for instance a company wanted to invite a limited number of beta testers, those beta testers would be able to create an arbitrary number of accounts themselves.
|
||||
|
||||
### Details
|
||||
|
||||
This vulnerability has already been submitted over email, this security advisory serves as formalization towards broader information dissemination. This vulnerability pertains to the user context used in the default-user-settings-flow. /api/v3/flows/instances/default-user-settings-flow/execute/
|
||||
|
||||
### For more information
|
||||
|
||||
If you have any questions or comments about this advisory:
|
||||
|
||||
- Email us at [security@goauthentik.io](mailto:security@goauthentik.io)
|
5
website/docs/security/policy.mdx
Normal file
5
website/docs/security/policy.mdx
Normal file
@ -0,0 +1,5 @@
|
||||
# Security Policy
|
||||
|
||||
import SecurityPolicy from "../../../SECURITY.md";
|
||||
|
||||
<SecurityPolicy />
|
117
website/integrations/services/truecommand/index.md
Normal file
117
website/integrations/services/truecommand/index.md
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
title: TrueNAS TrueCommand
|
||||
---
|
||||
|
||||
<span class="badge badge--secondary">Support level: Community</span>
|
||||
|
||||
## What is TrueNAS TrueCommand
|
||||
|
||||
From https://www.truenas.com/truecommand/
|
||||
:::note
|
||||
What is TrueCommand?
|
||||
TrueCommand is a ZFS-aware solution allowing you to set custom alerts on statistics like ARC usage or pool capacity and ensuring storag
|
||||
e uptime and future planning. TrueCommand also identifies and pinpoints errors on drives or vdevs (RAID groups), saving you valuable ti
|
||||
me when resolving issues.
|
||||
:::
|
||||
|
||||
:::warning
|
||||
This setup assumes you will be using HTTPS as TrueCommand generates ACS and Redirect URLs based on the complete URL.
|
||||
:::
|
||||
|
||||
## Preparation
|
||||
|
||||
The following placeholders will be used:
|
||||
|
||||
- `truecommand.company` is the FQDN of the snipe-it install.
|
||||
- `authentik.company` is the FQDN of the authentik install.
|
||||
|
||||
Create an application in authentik and use the slug for later as `truenas-truecommand`.
|
||||
|
||||
Create a SAML provider with the following parameters:
|
||||
|
||||
- ACS URL: `https://truecommand.company/saml/acs`
|
||||
- Issuer: `truecommand-saml`
|
||||
- Binding: `Post`
|
||||
|
||||
Under _Advanced protocol settings_, set a certificate for _Signing Certificate_.
|
||||
Under _Advanced protocol settings_, set NameID Property to _authentik default SAML Mapping: Email_.
|
||||
|
||||
## SAML Property Mappings
|
||||
|
||||
The following custom property mappings are required.
|
||||
|
||||
Under _Customisation_, select _Property Mappings_, then _Create_. Select _SAML Property Mapping_.
|
||||
|
||||
### Username
|
||||
|
||||
- Name: `Truecommand - Username`
|
||||
- SAML Attribute Name: `unique_name`
|
||||
- Expression
|
||||
|
||||
```python
|
||||
return request.user.username
|
||||
```
|
||||
|
||||
### Email
|
||||
|
||||
- Name: `Truecommand - Email`
|
||||
- SAML Attribute Name: `email`
|
||||
- Expression
|
||||
|
||||
```python
|
||||
return request.user.email
|
||||
```
|
||||
|
||||
### Fullname
|
||||
|
||||
- Name: `Truecommand - Fullname`
|
||||
- SAML Attribute Name: `given_name` OR `display_name`
|
||||
- Expression
|
||||
|
||||
```python
|
||||
return request.user.name
|
||||
```
|
||||
|
||||
### Other Attributes
|
||||
|
||||
If you have custom attributes, or attributes imported from Active Directory, TrueCommand supports the following additional mappings:
|
||||
|
||||
#### Role
|
||||
|
||||
- Name: `Truecommand - Role`
|
||||
- SAML Attribute Name: `title`
|
||||
- Expression
|
||||
|
||||
```python
|
||||
return [custom_attribute]
|
||||
```
|
||||
|
||||
#### Phone Number
|
||||
|
||||
- Name: `Truecommand - Phone Number`
|
||||
- SAML Attribute Name: `telephone_number`
|
||||
- Expression
|
||||
|
||||
```python
|
||||
return [custom_attribute]
|
||||
```
|
||||
|
||||
Return to _Providers_ under _Applications_, and edit the Provider created above.
|
||||
|
||||
Under _Advanced protocol settings_, select the additional property mappings created above.
|
||||
|
||||
### SAML Metadata
|
||||
|
||||
Click the _Copy download URL_ to save the Metadata URL into your clipboard.
|
||||
|
||||
## TrueCommand Config
|
||||
|
||||
- Click on the gear icon in the upper right corner.
|
||||
- Select Administration
|
||||
- Click on CONFIGURE
|
||||
- SAML Identity Provider URL: `Paste the Metadata URL from your clipboard.`
|
||||
- Click _Save_, then click _Configure_ again then select _Start the SAML service_, then click _Save_ to start the service.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- https://www.truenas.com/docs/truecommand/administration/settings/samlad/
|
@ -58,15 +58,19 @@ Requires authentik 2021.12.5.
|
||||
|
||||
To check if the user is member of an organisation, you can use the following policy on your flows:
|
||||
|
||||
:::info
|
||||
Make sure to include `read:org` in the sources' _Scopes_ setting.
|
||||
:::
|
||||
|
||||
```python
|
||||
# Ensure flow is only run during oauth logins via Github
|
||||
if context['source'].provider_type != "github":
|
||||
if context["source"].provider_type != "github":
|
||||
return True
|
||||
|
||||
accepted_org = "foo"
|
||||
|
||||
# Get the user-source connection object from the context, and get the access token
|
||||
connection = context['goauthentik.io/sources/connection']
|
||||
connection = context["goauthentik.io/sources/connection"]
|
||||
access_token = connection.access_token
|
||||
|
||||
# We also access the user info authentik already retrieved, to get the correct username
|
||||
@ -74,13 +78,15 @@ github_username = context["oauth_userinfo"]
|
||||
|
||||
# Github does not include Organisations in the userinfo endpoint, so we have to call another URL
|
||||
|
||||
orgs = requests.get(
|
||||
orgs_response = requests.get(
|
||||
"https://api.github.com/user/orgs",
|
||||
auth=(github_username["login"], access_token),
|
||||
headers={
|
||||
"accept": "application/vnd.github.v3+json"
|
||||
}
|
||||
).json()
|
||||
)
|
||||
orgs_response.raise_for_status()
|
||||
orgs = orgs_response.json()
|
||||
|
||||
# `orgs` will be formatted like this
|
||||
# [
|
||||
|
@ -281,5 +281,20 @@ module.exports = {
|
||||
"troubleshooting/missing_admin_group",
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "category",
|
||||
label: "Security",
|
||||
link: {
|
||||
type: "generated-index",
|
||||
title: "Security",
|
||||
slug: "security",
|
||||
},
|
||||
items: [
|
||||
"security/policy",
|
||||
"security/CVE-2022-46145",
|
||||
"security/CVE-2022-46172",
|
||||
"security/CVE-2022-23555",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -75,6 +75,7 @@ module.exports = {
|
||||
"services/pgadmin/index",
|
||||
"services/powerdns-admin/index",
|
||||
"services/snipe-it/index",
|
||||
"services/truecommand/index",
|
||||
"services/veeam-enterprise-manager/index",
|
||||
],
|
||||
},
|
||||
|
Reference in New Issue
Block a user