diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2138920ae..a448a83fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,6 +59,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v1 + - name: prepare ts api client + run: | + docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli generate -i /local/swagger.yaml -g typescript-fetch -o /local/web/src/api --additional-properties=typescriptThreePlus=true - name: Docker Login Registry env: DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/Dockerfile b/Dockerfile index 5041486ca..a806fb659 100644 --- a/Dockerfile +++ b/Dockerfile @@ -45,4 +45,5 @@ COPY ./lifecycle/ /lifecycle USER authentik STOPSIGNAL SIGINT ENV TMPDIR /dev/shm/ +ENV PYTHONUBUFFERED 1 ENTRYPOINT [ "/lifecycle/bootstrap.sh" ] diff --git a/authentik/admin/api/metrics.py b/authentik/admin/api/metrics.py index d3e9f812e..ad3943e16 100644 --- a/authentik/admin/api/metrics.py +++ b/authentik/admin/api/metrics.py @@ -7,8 +7,8 @@ from django.db.models import Count, ExpressionWrapper, F, Model from django.db.models.fields import DurationField from django.db.models.functions import ExtractHour from django.utils.timezone import now -from drf_yasg2.utils import swagger_auto_schema -from rest_framework.fields import SerializerMethodField +from drf_yasg2.utils import swagger_auto_schema, swagger_serializer_method +from rest_framework.fields import IntegerField, SerializerMethodField from rest_framework.permissions import IsAdminUser from rest_framework.request import Request from rest_framework.response import Response @@ -37,23 +37,39 @@ def get_events_per_1h(**filter_kwargs) -> list[dict[str, int]]: for hour in range(0, -24, -1): results.append( { - "x": time.mktime((_now + timedelta(hours=hour)).timetuple()) * 1000, - "y": data[hour * -1], + "x_cord": time.mktime((_now + timedelta(hours=hour)).timetuple()) + * 1000, + "y_cord": data[hour * -1], } ) return results -class AdministrationMetricsSerializer(Serializer): +class CoordinateSerializer(Serializer): + """Coordinates for diagrams""" + + x_cord = IntegerField(read_only=True) + y_cord = IntegerField(read_only=True) + + def create(self, validated_data: dict) -> Model: + raise NotImplementedError + + def update(self, instance: Model, validated_data: dict) -> Model: + raise NotImplementedError + + +class LoginMetricsSerializer(Serializer): """Login Metrics per 1h""" logins_per_1h = SerializerMethodField() logins_failed_per_1h = SerializerMethodField() + @swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True)) def get_logins_per_1h(self, _): """Get successful logins per hour for the last 24 hours""" return get_events_per_1h(action=EventAction.LOGIN) + @swagger_serializer_method(serializer_or_field=CoordinateSerializer(many=True)) def get_logins_failed_per_1h(self, _): """Get failed logins per hour for the last 24 hours""" return get_events_per_1h(action=EventAction.LOGIN_FAILED) @@ -70,8 +86,8 @@ class AdministrationMetricsViewSet(ViewSet): permission_classes = [IsAdminUser] - @swagger_auto_schema(responses={200: AdministrationMetricsSerializer(many=True)}) + @swagger_auto_schema(responses={200: LoginMetricsSerializer(many=False)}) def list(self, request: Request) -> Response: """Login Metrics per 1h""" - serializer = AdministrationMetricsSerializer(True) + serializer = LoginMetricsSerializer(True) return Response(serializer.data) diff --git a/authentik/admin/api/tasks.py b/authentik/admin/api/tasks.py index 3dca24bc1..d3abd5dea 100644 --- a/authentik/admin/api/tasks.py +++ b/authentik/admin/api/tasks.py @@ -25,8 +25,8 @@ class TaskSerializer(Serializer): task_finish_timestamp = DateTimeField(source="finish_timestamp") status = ChoiceField( - source="result.status.value", - choices=[(x.value, x.name) for x in TaskResultStatus], + source="result.status.name", + choices=[(x.name, x.name) for x in TaskResultStatus], ) messages = ListField(source="result.messages") diff --git a/authentik/api/v2/urls.py b/authentik/api/v2/urls.py index 8ea2ec4ae..9f3fd39e7 100644 --- a/authentik/api/v2/urls.py +++ b/authentik/api/v2/urls.py @@ -1,4 +1,5 @@ """api v2 urls""" +from django.conf import settings from django.urls import path, re_path from drf_yasg2 import openapi from drf_yasg2.views import get_schema_view @@ -156,6 +157,14 @@ router.register("stages/user_write", UserWriteStageViewSet) router.register("stages/dummy", DummyStageViewSet) router.register("policies/dummy", DummyPolicyViewSet) +api_urls = router.urls + [ + path( + "flows/executor//", + FlowExecutorView.as_view(), + name="flow-executor", + ), +] + info = openapi.Info( title="authentik API", default_version="v2", @@ -165,26 +174,22 @@ info = openapi.Info( ), ) SchemaView = get_schema_view( - info, - public=True, - permission_classes=(AllowAny,), + info, public=True, permission_classes=(AllowAny,), patterns=api_urls ) -urlpatterns = [ +urlpatterns = api_urls + [ re_path( r"^swagger(?P\.json|\.yaml)$", SchemaView.without_ui(cache_timeout=0), name="schema-json", ), - path( - "swagger/", - SchemaView.with_ui("swagger", cache_timeout=0), - name="schema-swagger-ui", - ), - path("redoc/", SchemaView.with_ui("redoc", cache_timeout=0), name="schema-redoc"), - path( - "flows/executor//", - FlowExecutorView.as_view(), - name="flow-executor", - ), -] + router.urls +] + +if settings.DEBUG: + urlpatterns = urlpatterns + [ + path( + "swagger/", + SchemaView.with_ui("swagger", cache_timeout=0), + name="schema-swagger-ui", + ), + ] diff --git a/authentik/core/api/applications.py b/authentik/core/api/applications.py index 76cc432b9..eae9fc986 100644 --- a/authentik/core/api/applications.py +++ b/authentik/core/api/applications.py @@ -2,6 +2,7 @@ from django.core.cache import cache from django.db.models import QuerySet from django.http.response import Http404 +from drf_yasg2.utils import swagger_auto_schema from guardian.shortcuts import get_objects_for_user from rest_framework.decorators import action from rest_framework.fields import SerializerMethodField @@ -13,7 +14,7 @@ from rest_framework.viewsets import ModelViewSet from rest_framework_guardian.filters import ObjectPermissionsFilter from structlog.stdlib import get_logger -from authentik.admin.api.metrics import get_events_per_1h +from authentik.admin.api.metrics import CoordinateSerializer, get_events_per_1h from authentik.core.api.providers import ProviderSerializer from authentik.core.models import Application from authentik.events.models import EventAction @@ -109,6 +110,7 @@ class ApplicationViewSet(ModelViewSet): serializer = self.get_serializer(allowed_applications, many=True) return self.get_paginated_response(serializer.data) + @swagger_auto_schema(responses={200: CoordinateSerializer(many=True)}) @action(detail=True) def metrics(self, request: Request, slug: str): """Metrics for application logins""" diff --git a/authentik/core/api/utils.py b/authentik/core/api/utils.py index b93f75155..438efa2df 100644 --- a/authentik/core/api/utils.py +++ b/authentik/core/api/utils.py @@ -28,9 +28,9 @@ class MetaNameSerializer(Serializer): class TypeCreateSerializer(Serializer): """Types of an object that can be created""" - name = CharField(read_only=True) - description = CharField(read_only=True) - link = CharField(read_only=True) + name = CharField(required=True) + description = CharField(required=True) + link = CharField(required=True) def create(self, validated_data: dict) -> Model: raise NotImplementedError diff --git a/authentik/core/templates/login/base_full.html b/authentik/core/templates/login/base_full.html index 6f58568d6..22c343ea9 100644 --- a/authentik/core/templates/login/base_full.html +++ b/authentik/core/templates/login/base_full.html @@ -16,6 +16,16 @@ {% block body %}
+ + + + + + + + + +

${item.body}

- ${created.toLocaleString()} + ${item.created?.toLocaleString()} `; } diff --git a/web/src/elements/policies/BoundPoliciesList.ts b/web/src/elements/policies/BoundPoliciesList.ts index 03d198cbe..c608b5149 100644 --- a/web/src/elements/policies/BoundPoliciesList.ts +++ b/web/src/elements/policies/BoundPoliciesList.ts @@ -2,16 +2,16 @@ import { gettext } from "django"; import { customElement, html, property, TemplateResult } from "lit-element"; import { AKResponse } from "../../api/Client"; import { Table, TableColumn } from "../../elements/table/Table"; -import { PolicyBinding } from "../../api/PolicyBindings"; +import { PoliciesApi, PolicyBinding } from "../../api"; import "../../elements/Tabs"; -import "../../elements/AdminLoginsChart"; import "../../elements/buttons/ModalButton"; import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/Dropdown"; -import { Policy } from "../../api/Policies"; import { until } from "lit-html/directives/until"; import { PAGE_SIZE } from "../../constants"; +import { DEFAULT_CONFIG } from "../../api/Config"; +import { AdminURLManager } from "../../api/legacy"; @customElement("ak-bound-policies-list") export class BoundPoliciesList extends Table { @@ -19,11 +19,11 @@ export class BoundPoliciesList extends Table { target?: string; apiEndpoint(page: number): Promise> { - return PolicyBinding.list({ + return new PoliciesApi(DEFAULT_CONFIG).policiesBindingsList({ target: this.target || "", ordering: "order", page: page, - page_size: PAGE_SIZE, + pageSize: PAGE_SIZE, }); } @@ -56,13 +56,13 @@ export class BoundPoliciesList extends Table { html`${item.order}`, html`${item.timeout}`, html` - + ${gettext("Edit")}
- + ${gettext("Delete")} @@ -78,7 +78,7 @@ export class BoundPoliciesList extends Table { ${gettext("No policies are currently bound to this object.")}
- + ${gettext("Bind Policy")} @@ -96,7 +96,7 @@ export class BoundPoliciesList extends Table { - + ${gettext("Bind Policy")} diff --git a/web/src/elements/sidebar/SidebarBrand.ts b/web/src/elements/sidebar/SidebarBrand.ts index e55e17edd..1d9e5eef7 100644 --- a/web/src/elements/sidebar/SidebarBrand.ts +++ b/web/src/elements/sidebar/SidebarBrand.ts @@ -3,15 +3,17 @@ import { css, CSSResult, customElement, html, LitElement, property, TemplateResu import PageStyle from "@patternfly/patternfly/components/Page/page.css"; // @ts-ignore import GlobalsStyle from "@patternfly/patternfly/base/patternfly-globals.css"; -import { Config } from "../../api/Config"; +import { configureSentry } from "../../api/Config"; +import { Config } from "../../api"; +import { ifDefined } from "lit-html/directives/if-defined"; export const DefaultConfig: Config = { - branding_logo: " /static/dist/assets/icons/icon_left_brand.svg", - branding_title: "authentik", + brandingLogo: " /static/dist/assets/icons/icon_left_brand.svg", + brandingTitle: "authentik", - error_reporting_enabled: false, - error_reporting_environment: "", - error_reporting_send_pii: false, + errorReportingEnabled: false, + errorReportingEnvironment: "", + errorReportingSendPii: false, }; @customElement("ak-sidebar-brand") @@ -40,13 +42,13 @@ export class SidebarBrand extends LitElement { } firstUpdated(): void { - Config.get().then((c) => (this.config = c)); + configureSentry().then((c) => {this.config = c;}); } render(): TemplateResult { return html`
- authentik icon + authentik icon
`; } diff --git a/web/src/elements/sidebar/SidebarUser.ts b/web/src/elements/sidebar/SidebarUser.ts index 01a75d470..fc17cfbde 100644 --- a/web/src/elements/sidebar/SidebarUser.ts +++ b/web/src/elements/sidebar/SidebarUser.ts @@ -5,10 +5,11 @@ import NavStyle from "@patternfly/patternfly/components/Nav/nav.css"; import fa from "@fortawesome/fontawesome-free/css/all.css"; // @ts-ignore import AvatarStyle from "@patternfly/patternfly/components/Avatar/avatar.css"; -import { User } from "../../api/Users"; +import { me } from "../../api/Users"; import { until } from "lit-html/directives/until"; import "../notifications/NotificationTrigger"; +import { ifDefined } from "lit-html/directives/if-defined"; @customElement("ak-sidebar-user") export class SidebarUser extends LitElement { @@ -37,8 +38,8 @@ export class SidebarUser extends LitElement { render(): TemplateResult { return html` - ${until(User.me().then((u) => { - return html``; + ${until(me().then((u) => { + return html``; }), html``)} diff --git a/web/src/elements/table/Table.ts b/web/src/elements/table/Table.ts index f6c4a2b22..c40412425 100644 --- a/web/src/elements/table/Table.ts +++ b/web/src/elements/table/Table.ts @@ -5,7 +5,7 @@ import { COMMON_STYLES } from "../../common/styles"; import "./TablePagination"; import "../EmptyState"; - +import "../Spinner"; export class TableColumn { diff --git a/web/src/elements/table/TablePagination.ts b/web/src/elements/table/TablePagination.ts index fbcae2f27..0bfee7c84 100644 --- a/web/src/elements/table/TablePagination.ts +++ b/web/src/elements/table/TablePagination.ts @@ -1,12 +1,12 @@ import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element"; import { COMMON_STYLES } from "../../common/styles"; -import { PBPagination } from "../../api/Client"; +import { AKPagination } from "../../api/Client"; import { gettext } from "django"; @customElement("ak-table-pagination") export class TablePagination extends LitElement { @property({attribute: false}) - pages?: PBPagination; + pages?: AKPagination; @property({attribute: false}) // eslint-disable-next-line @@ -22,8 +22,8 @@ export class TablePagination extends LitElement {
- ${this.pages?.start_index} - - ${this.pages?.end_index} of + ${this.pages?.startIndex} - + ${this.pages?.endIndex} of ${this.pages?.count}
diff --git a/web/src/flow.ts b/web/src/flow.ts index 202eaae2f..0c7d9baac 100644 --- a/web/src/flow.ts +++ b/web/src/flow.ts @@ -1,3 +1,3 @@ import "construct-style-sheets-polyfill"; -import "./pages/generic/FlowExecutor"; +import "./flows/FlowExecutor"; diff --git a/web/src/pages/generic/FlowExecutor.ts b/web/src/flows/FlowExecutor.ts similarity index 64% rename from web/src/pages/generic/FlowExecutor.ts rename to web/src/flows/FlowExecutor.ts index c2de4caa4..fa8526ccc 100644 --- a/web/src/pages/generic/FlowExecutor.ts +++ b/web/src/flows/FlowExecutor.ts @@ -1,34 +1,34 @@ import { gettext } from "django"; import { LitElement, html, customElement, property, TemplateResult, CSSResult, css } from "lit-element"; import { unsafeHTML } from "lit-html/directives/unsafe-html"; -import { getCookie } from "../../utils"; -import "../../elements/stages/authenticator_static/AuthenticatorStaticStage"; -import "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage"; -import "../../elements/stages/authenticator_validate/AuthenticatorValidateStage"; -import "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; -import "../../elements/stages/autosubmit/AutosubmitStage"; -import "../../elements/stages/captcha/CaptchaStage"; -import "../../elements/stages/consent/ConsentStage"; -import "../../elements/stages/email/EmailStage"; -import "../../elements/stages/identification/IdentificationStage"; -import "../../elements/stages/password/PasswordStage"; -import "../../elements/stages/prompt/PromptStage"; -import { ShellChallenge, Challenge, ChallengeTypes, Flow, RedirectChallenge } from "../../api/Flows"; -import { DefaultClient } from "../../api/Client"; -import { IdentificationChallenge } from "../../elements/stages/identification/IdentificationStage"; -import { PasswordChallenge } from "../../elements/stages/password/PasswordStage"; -import { ConsentChallenge } from "../../elements/stages/consent/ConsentStage"; -import { EmailChallenge } from "../../elements/stages/email/EmailStage"; -import { AutosubmitChallenge } from "../../elements/stages/autosubmit/AutosubmitStage"; -import { PromptChallenge } from "../../elements/stages/prompt/PromptStage"; -import { AuthenticatorTOTPChallenge } from "../../elements/stages/authenticator_totp/AuthenticatorTOTPStage"; -import { AuthenticatorStaticChallenge } from "../../elements/stages/authenticator_static/AuthenticatorStaticStage"; -import { AuthenticatorValidateStageChallenge } from "../../elements/stages/authenticator_validate/AuthenticatorValidateStage"; -import { WebAuthnAuthenticatorRegisterChallenge } from "../../elements/stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; -import { CaptchaChallenge } from "../../elements/stages/captcha/CaptchaStage"; -import { COMMON_STYLES } from "../../common/styles"; -import { SpinnerSize } from "../../elements/Spinner"; -import { StageHost } from "../../elements/stages/base"; +import "./stages/authenticator_static/AuthenticatorStaticStage"; +import "./stages/authenticator_totp/AuthenticatorTOTPStage"; +import "./stages/authenticator_validate/AuthenticatorValidateStage"; +import "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; +import "./stages/autosubmit/AutosubmitStage"; +import "./stages/captcha/CaptchaStage"; +import "./stages/consent/ConsentStage"; +import "./stages/email/EmailStage"; +import "./stages/identification/IdentificationStage"; +import "./stages/password/PasswordStage"; +import "./stages/prompt/PromptStage"; +import { ShellChallenge, RedirectChallenge } from "../api/Flows"; +import { IdentificationChallenge } from "./stages/identification/IdentificationStage"; +import { PasswordChallenge } from "./stages/password/PasswordStage"; +import { ConsentChallenge } from "./stages/consent/ConsentStage"; +import { EmailChallenge } from "./stages/email/EmailStage"; +import { AutosubmitChallenge } from "./stages/autosubmit/AutosubmitStage"; +import { PromptChallenge } from "./stages/prompt/PromptStage"; +import { AuthenticatorTOTPChallenge } from "./stages/authenticator_totp/AuthenticatorTOTPStage"; +import { AuthenticatorStaticChallenge } from "./stages/authenticator_static/AuthenticatorStaticStage"; +import { AuthenticatorValidateStageChallenge } from "./stages/authenticator_validate/AuthenticatorValidateStage"; +import { WebAuthnAuthenticatorRegisterChallenge } from "./stages/authenticator_webauthn/WebAuthnAuthenticatorRegisterStage"; +import { CaptchaChallenge } from "./stages/captcha/CaptchaStage"; +import { COMMON_STYLES } from "../common/styles"; +import { SpinnerSize } from "../elements/Spinner"; +import { StageHost } from "./stages/base"; +import { Challenge, ChallengeTypeEnum, FlowsApi } from "../api"; +import { DEFAULT_CONFIG } from "../api/Config"; @customElement("ak-flow-executor") export class FlowExecutor extends LitElement implements StageHost { @@ -68,37 +68,30 @@ export class FlowExecutor extends LitElement implements StageHost { }); } - submit(formData?: FormData): Promise { - const csrftoken = getCookie("authentik_csrf"); - const request = new Request(DefaultClient.makeUrl(["flows", "executor", this.flowSlug]), { - headers: { - "X-CSRFToken": csrftoken, - }, - }); + submit(formData?: T): Promise { this.loading = true; - return fetch(request, { - method: "POST", - mode: "same-origin", - body: formData, - }) - .then((response) => { - return response.json(); - }) - .then((data) => { - this.challenge = data; - }) - .catch((e) => { - this.errorMessage(e); - }) - .finally(() => { - this.loading = false; - }); + return new FlowsApi(DEFAULT_CONFIG).flowsExecutorSolveRaw({ + flowSlug: this.flowSlug, + data: formData || {}, + }).then((challengeRaw) => { + return challengeRaw.raw.json(); + }).then((data) => { + this.challenge = data; + }).catch((e) => { + this.errorMessage(e); + }).finally(() => { + this.loading = false; + }); } firstUpdated(): void { this.loading = true; - Flow.executor(this.flowSlug).then((challenge) => { - this.challenge = challenge; + new FlowsApi(DEFAULT_CONFIG).flowsExecutorGetRaw({ + flowSlug: this.flowSlug + }).then((challengeRaw) => { + return challengeRaw.raw.json(); + }).then((challenge) => { + this.challenge = challenge as Challenge; }).catch((e) => { // Catch JSON or Update errors this.errorMessage(e); @@ -109,7 +102,7 @@ export class FlowExecutor extends LitElement implements StageHost { errorMessage(error: string): void { this.challenge = { - type: ChallengeTypes.shell, + type: ChallengeTypeEnum.Shell, body: `