web: remove policy bindings page (#370)

* admin: accept ?target for PolicyBindingCreateView

* core: fix rendering of hidden fields in horizontal form

* web: add create button for application's bound policies

* admin: fix delete form not working

* web: fix ak-refresh event not being dispatched correctly

* web: fix linting errors

* admin: fix tests not loading

* build(deps-dev): bump eslint from 7.14.0 to 7.15.0 in /web (#372)

Bumps [eslint](https://github.com/eslint/eslint) from 7.14.0 to 7.15.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v7.14.0...v7.15.0)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump rollup from 2.34.1 to 2.34.2 in /web (#373)

Bumps [rollup](https://github.com/rollup/rollup) from 2.34.1 to 2.34.2.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.34.1...v2.34.2)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps): bump @types/codemirror from 0.0.100 to 0.0.102 in /web (#374)

Bumps [@types/codemirror](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/codemirror) from 0.0.100 to 0.0.102.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/codemirror)

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* build(deps-dev): bump bandit from 1.6.2 to 1.6.3 (#371)

* build(deps-dev): bump bandit from 1.6.2 to 1.6.3

Bumps [bandit](https://github.com/PyCQA/bandit) from 1.6.2 to 1.6.3.
- [Release notes](https://github.com/PyCQA/bandit/releases)
- [Commits](https://github.com/PyCQA/bandit/compare/1.6.2...1.6.3)

Signed-off-by: dependabot[bot] <support@github.com>

* root: update for new bandit version

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>

* web: add header to bound-policies

* web: fix spacing between bulk_select buttons

* web: add separate ak-bound-policies-list, add flow view page

* web: fix flows' policies not loading

* Squashed commit of the following:

commit e535cb0ec8
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Thu Dec 10 09:58:07 2020 +0100

    build(deps): bump boto3 from 1.16.32 to 1.16.33 (#383)

    Bumps [boto3](https://github.com/boto/boto3) from 1.16.32 to 1.16.33.
    - [Release notes](https://github.com/boto/boto3/releases)
    - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
    - [Commits](https://github.com/boto/boto3/compare/1.16.32...1.16.33)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 8c1f55d3e3
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Wed Dec 9 09:06:45 2020 +0100

    build(deps): bump boto3 from 1.16.31 to 1.16.32 (#382)

    Bumps [boto3](https://github.com/boto/boto3) from 1.16.31 to 1.16.32.
    - [Release notes](https://github.com/boto/boto3/releases)
    - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
    - [Commits](https://github.com/boto/boto3/compare/1.16.31...1.16.32)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit c3a2cb44cd
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Wed Dec 9 09:06:29 2020 +0100

    build(deps): bump celery from 5.0.3 to 5.0.4 (#380)

    Bumps [celery](https://github.com/celery/celery) from 5.0.3 to 5.0.4.
    - [Release notes](https://github.com/celery/celery/releases)
    - [Changelog](https://github.com/celery/celery/blob/master/Changelog.rst)
    - [Commits](https://github.com/celery/celery/compare/v5.0.3...v5.0.4)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 682401bbf2
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Wed Dec 9 07:20:45 2020 +0100

    build(deps): bump uvicorn from 0.12.3 to 0.13.0 (#381)

    Bumps [uvicorn](https://github.com/encode/uvicorn) from 0.12.3 to 0.13.0.
    - [Release notes](https://github.com/encode/uvicorn/releases)
    - [Changelog](https://github.com/encode/uvicorn/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/encode/uvicorn/compare/0.12.3...0.13.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 3e6e167348
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 10:32:00 2020 +0100

    build(deps-dev): bump @typescript-eslint/parser in /web (#377)

    Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 4.9.0 to 4.9.1.
    - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
    - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md)
    - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.9.1/packages/parser)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit d08c1b7b02
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 10:31:47 2020 +0100

    build(deps): bump @sentry/browser from 5.28.0 to 5.29.0 in /web (#378)

    Bumps [@sentry/browser](https://github.com/getsentry/sentry-javascript) from 5.28.0 to 5.29.0.
    - [Release notes](https://github.com/getsentry/sentry-javascript/releases)
    - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/getsentry/sentry-javascript/compare/5.28.0...5.29.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 94d70d252c
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 09:02:37 2020 +0100

    build(deps): bump boto3 from 1.16.30 to 1.16.31 (#375)

    Bumps [boto3](https://github.com/boto/boto3) from 1.16.30 to 1.16.31.
    - [Release notes](https://github.com/boto/boto3/releases)
    - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst)
    - [Commits](https://github.com/boto/boto3/compare/1.16.30...1.16.31)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit ccfe746dd5
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 09:02:28 2020 +0100

    build(deps-dev): bump @typescript-eslint/eslint-plugin in /web (#376)

    Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 4.9.0 to 4.9.1.
    - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
    - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md)
    - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v4.9.1/packages/eslint-plugin)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit ef5dffa96a
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Tue Dec 8 09:02:16 2020 +0100

    build(deps): bump @sentry/tracing from 5.28.0 to 5.29.0 in /web (#379)

    Bumps [@sentry/tracing](https://github.com/getsentry/sentry-javascript) from 5.28.0 to 5.29.0.
    - [Release notes](https://github.com/getsentry/sentry-javascript/releases)
    - [Changelog](https://github.com/getsentry/sentry-javascript/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/getsentry/sentry-javascript/compare/5.28.0...5.29.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 2caa1e7650
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 11:21:07 2020 +0100

    build(deps-dev): bump bandit from 1.6.2 to 1.6.3 (#371)

    * build(deps-dev): bump bandit from 1.6.2 to 1.6.3

    Bumps [bandit](https://github.com/PyCQA/bandit) from 1.6.2 to 1.6.3.
    - [Release notes](https://github.com/PyCQA/bandit/releases)
    - [Commits](https://github.com/PyCQA/bandit/compare/1.6.2...1.6.3)

    Signed-off-by: dependabot[bot] <support@github.com>

    * root: update for new bandit version

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
    Co-authored-by: Jens Langhammer <jens.langhammer@beryju.org>

commit 2246f3a534
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 10:26:01 2020 +0100

    build(deps): bump @types/codemirror from 0.0.100 to 0.0.102 in /web (#374)

    Bumps [@types/codemirror](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/codemirror) from 0.0.100 to 0.0.102.
    - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
    - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/codemirror)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 95ba00cb79
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 09:09:49 2020 +0100

    build(deps): bump rollup from 2.34.1 to 2.34.2 in /web (#373)

    Bumps [rollup](https://github.com/rollup/rollup) from 2.34.1 to 2.34.2.
    - [Release notes](https://github.com/rollup/rollup/releases)
    - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/rollup/rollup/compare/v2.34.1...v2.34.2)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

commit 2ab4d6620f
Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Date:   Mon Dec 7 09:09:24 2020 +0100

    build(deps-dev): bump eslint from 7.14.0 to 7.15.0 in /web (#372)

    Bumps [eslint](https://github.com/eslint/eslint) from 7.14.0 to 7.15.0.
    - [Release notes](https://github.com/eslint/eslint/releases)
    - [Changelog](https://github.com/eslint/eslint/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/eslint/eslint/compare/v7.14.0...v7.15.0)

    Signed-off-by: dependabot[bot] <support@github.com>

    Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* web: fix linting error

* web: simplify sidebar logic

* web: add support for multiple active matchers per sidebar item

* web: move router to elements

* flows: add stage_obj to flows api

* sources/*: make all sources implement SerializerModel

* web: improve listing of stages

* web: implement expandable table

* web/table: use TemplateResult as return value for row()

* web: add empty state, fix link for BoundStageList

* admin: make stage binding form accept ?target like policy binding

* web: fix styles in dark mode for expanding tables

* flows: add policybindingmodel_ptr_id to FlowStageBinding API

* web: improve wording for policies

* web: fix dark theme for tertiary buttons and static modals

* web: implement SourceViewPage

* web: add empty state for BoundPoliciesList

* web: cleanup URLs for FlowStageBindings

* root: remove url attribute from ak-messages

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Jens L 2020-12-12 19:39:09 +01:00 committed by GitHub
parent e6a776be07
commit 488e8f769a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 896 additions and 269 deletions

View file

@ -53,10 +53,10 @@
{% for flow in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<a href="/flows/{{ flow.slug }}/">
<div><code>{{ flow.slug }}</code></div>
<small>{{ flow.name }}</small>
</div>
</a>
</th>
<td role="cell">
<span>

View file

@ -63,12 +63,12 @@
{% for source in object_list %}
<tr role="row">
<th role="columnheader">
<div>
<a href="/sources/{{ source.slug }}/">
<div>{{ source.name }}</div>
{% if not source.enabled %}
<small>{% trans 'Disabled' %}</small>
{% endif %}
</div>
</a>
</th>
<td role="cell">
<span>

View file

View file

@ -0,0 +1,26 @@
"""admin tests"""
from django.test import TestCase
from django.test.client import RequestFactory
from authentik.admin.views.policies_bindings import PolicyBindingCreateView
from authentik.core.models import Application
class TestPolicyBindingView(TestCase):
"""Generic admin tests"""
def setUp(self):
self.factory = RequestFactory()
def test_without_get_param(self):
"""Test PolicyBindingCreateView without get params"""
request = self.factory.get("/")
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_param(self):
"""Test PolicyBindingCreateView with get params"""
target = Application.objects.create(name="test")
request = self.factory.get("/", {"target": target.pk.hex})
view = PolicyBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {"target": target, "order": 0})

View file

@ -0,0 +1,26 @@
"""admin tests"""
from django.test import TestCase
from django.test.client import RequestFactory
from authentik.admin.views.stages_bindings import StageBindingCreateView
from authentik.flows.models import Flow
class TestStageBindingView(TestCase):
"""Generic admin tests"""
def setUp(self):
self.factory = RequestFactory()
def test_without_get_param(self):
"""Test StageBindingCreateView without get params"""
request = self.factory.get("/")
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {})
def test_with_param(self):
"""Test StageBindingCreateView with get params"""
target = Flow.objects.create(name="test", slug="test")
request = self.factory.get("/", {"target": target.pk.hex})
view = StageBindingCreateView(request=request)
self.assertEqual(view.get_initial(), {"target": target, "order": 0})

View file

@ -1,10 +1,12 @@
"""authentik PolicyBinding administration"""
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import QuerySet
from django.db.models import Max, QuerySet
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
@ -18,7 +20,7 @@ from authentik.admin.views.utils import (
)
from authentik.lib.views import CreateAssignPermView
from authentik.policies.forms import PolicyBindingForm
from authentik.policies.models import PolicyBinding
from authentik.policies.models import PolicyBinding, PolicyBindingModel
class PolicyBindingListView(
@ -67,6 +69,22 @@ class PolicyBindingCreateView(
success_url = reverse_lazy("authentik_admin:policies-bindings")
success_message = _("Successfully created PolicyBinding")
def get_initial(self) -> dict[str, Any]:
if "target" in self.request.GET:
initial_target_pk = self.request.GET["target"]
targets = PolicyBindingModel.objects.filter(
pk=initial_target_pk
).select_subclasses()
if not targets.exists():
return {}
max_order = PolicyBinding.objects.filter(target=targets.first()).aggregate(
Max("order")
)["order__max"]
if not isinstance(max_order, int):
max_order = -1
return {"target": targets.first(), "order": max_order + 1}
return super().get_initial()
class PolicyBindingUpdateView(
SuccessMessageMixin,

View file

@ -1,9 +1,12 @@
"""authentik StageBinding administration"""
from typing import Any
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.mixins import (
PermissionRequiredMixin as DjangoPermissionRequiredMixin,
)
from django.contrib.messages.views import SuccessMessageMixin
from django.db.models import Max
from django.urls import reverse_lazy
from django.utils.translation import gettext as _
from django.views.generic import ListView, UpdateView
@ -15,7 +18,7 @@ from authentik.admin.views.utils import (
UserPaginateListMixin,
)
from authentik.flows.forms import FlowStageBindingForm
from authentik.flows.models import FlowStageBinding
from authentik.flows.models import Flow, FlowStageBinding
from authentik.lib.views import CreateAssignPermView
@ -47,6 +50,20 @@ class StageBindingCreateView(
success_url = reverse_lazy("authentik_admin:stage-bindings")
success_message = _("Successfully created StageBinding")
def get_initial(self) -> dict[str, Any]:
if "target" in self.request.GET:
initial_target_pk = self.request.GET["target"]
targets = Flow.objects.filter(pk=initial_target_pk).select_subclasses()
if not targets.exists():
return {}
max_order = FlowStageBinding.objects.filter(
target=targets.first()
).aggregate(Max("order"))["order__max"]
if not isinstance(max_order, int):
max_order = -1
return {"target": targets.first(), "order": max_order + 1}
return super().get_initial()
class StageBindingUpdateView(
SuccessMessageMixin,

View file

@ -15,6 +15,12 @@ class SourceSerializer(ModelSerializer):
"""Get object type so that we know which API Endpoint to use to get the full object"""
return obj._meta.object_name.lower().replace("source", "")
def to_representation(self, instance: Source):
# pyright: reportGeneralTypeIssues=false
if instance.__class__ == Source:
return super().to_representation(instance)
return instance.serializer(instance=instance).data
class Meta:
model = Source
@ -26,6 +32,7 @@ class SourceViewSet(ReadOnlyModelViewSet):
queryset = Source.objects.all()
serializer_class = SourceSerializer
lookup_field = "slug"
def get_queryset(self):
return Source.objects.select_subclasses()

View file

@ -20,7 +20,7 @@ from authentik.core.exceptions import PropertyMappingExpressionException
from authentik.core.signals import password_changed
from authentik.core.types import UILoginButton
from authentik.flows.models import Flow
from authentik.lib.models import CreatedUpdatedModel
from authentik.lib.models import CreatedUpdatedModel, SerializerModel
from authentik.policies.models import PolicyBindingModel
LOGGER = get_logger()
@ -200,7 +200,7 @@ class Application(PolicyBindingModel):
verbose_name_plural = _("Applications")
class Source(PolicyBindingModel):
class Source(SerializerModel, PolicyBindingModel):
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
name = models.TextField(help_text=_("Source's display Name."))

View file

@ -20,7 +20,7 @@
<div class="pf-l-stack__item">
<div class="pf-c-card">
<div class="pf-c-card__body">
<form action="" method="post" class="pf-c-form">
<form id="delete-form" action="" method="post" class="pf-c-form">
{% csrf_token %}
<p>
{% blocktrans with object_type=object|verbose_name name=object %}
@ -35,7 +35,7 @@
</div>
</section>
<footer class="pf-c-modal-box__footer">
<input class="pf-c-button pf-m-danger" type="submit" value="{% trans 'Delete' %}" />
<input class="pf-c-button pf-m-danger" type="submit" form="delete-form" value="{% trans 'Delete' %}" />
<a class="pf-c-button pf-m-secondary" href="{% back %}">{% trans "Back" %}</a>
</footer>
{% endblock %}

View file

@ -1,7 +1,7 @@
{% load static %}
{% load i18n %}
<ak-messages url="{% url 'authentik_api:messages-list' %}"></ak-messages>
<ak-messages></ak-messages>
<header class="pf-c-login__main-header">
<h1 class="pf-c-title pf-m-3xl">

View file

@ -28,7 +28,7 @@
</filter>
</svg>
</div>
<ak-messages url="{% url 'authentik_api:messages-list' %}"></ak-messages>
<ak-messages></ak-messages>
<div class="pf-c-login">
<div class="pf-c-login__container">
<header class="pf-c-login__header">

View file

@ -3,6 +3,9 @@
{% csrf_token %}
{% for field in form %}
{% if field.field.widget|fieldtype == 'HiddenInput' %}
{{ field }}
{% else %}
<div class="pf-c-form__group {% if field.errors %} has-error {% endif %}">
{% if field.field.widget|fieldtype == 'RadioSelect' %}
<div class="pf-c-form__group-label">
@ -105,4 +108,5 @@
</p>
{% endfor %}
</div>
{% endif %}
{% endfor %}

View file

@ -21,6 +21,7 @@ class FlowSerializer(ModelSerializer):
model = Flow
fields = [
"pk",
"policybindingmodel_ptr_id",
"name",
"slug",
"title",
@ -37,31 +38,7 @@ class FlowViewSet(ModelViewSet):
queryset = Flow.objects.all()
serializer_class = FlowSerializer
class FlowStageBindingSerializer(ModelSerializer):
"""FlowStageBinding Serializer"""
class Meta:
model = FlowStageBinding
fields = [
"pk",
"target",
"stage",
"evaluate_on_plan",
"re_evaluate_policies",
"order",
"policies",
]
class FlowStageBindingViewSet(ModelViewSet):
"""FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all()
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"
lookup_field = "slug"
class StageSerializer(ModelSerializer):
@ -92,3 +69,32 @@ class StageViewSet(ReadOnlyModelViewSet):
def get_queryset(self):
return Stage.objects.select_subclasses()
class FlowStageBindingSerializer(ModelSerializer):
"""FlowStageBinding Serializer"""
stage_obj = StageSerializer(read_only=True, source="stage")
class Meta:
model = FlowStageBinding
fields = [
"pk",
"policybindingmodel_ptr_id",
"target",
"stage",
"stage_obj",
"evaluate_on_plan",
"re_evaluate_policies",
"order",
"policies",
]
class FlowStageBindingViewSet(ModelViewSet):
"""FlowStageBinding Viewset"""
queryset = FlowStageBinding.objects.all()
serializer_class = FlowStageBindingSerializer
filterset_fields = "__all__"

View file

@ -37,6 +37,11 @@ class FlowStageBindingForm(forms.ModelForm):
queryset=Stage.objects.all().select_subclasses(), to_field_name="stage_uuid"
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "target" in self.initial:
self.fields["target"].widget = forms.HiddenInput()
class Meta:
model = FlowStageBinding

View file

@ -20,6 +20,11 @@ class PolicyBindingForm(forms.ModelForm):
queryset=Policy.objects.all().select_subclasses(),
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if "target" in self.initial:
self.fields["target"].widget = forms.HiddenInput()
class Meta:
model = PolicyBinding

View file

@ -18,6 +18,8 @@ class ChannelsStorage(FallbackStorage):
def _store(self, messages: list[Message], response, *args, **kwargs):
prefix = f"user_{self.request.user.pk}_messages_"
keys = cache.keys(f"{prefix}*")
if len(keys) < 1:
return super()._store(messages, response, *args, **kwargs)
for key in keys:
uid = key.replace(prefix, "")
for message in messages:
@ -30,4 +32,4 @@ class ChannelsStorage(FallbackStorage):
"message": message.message,
},
)
return super()._store(messages, response, *args, **kwargs)
return None

View file

@ -7,6 +7,7 @@ from django.db import models
from django.forms import ModelForm
from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, Connection, Server
from rest_framework.serializers import Serializer
from authentik.core.models import Group, PropertyMapping, Source
from authentik.lib.models import DomainlessURLValidator
@ -73,6 +74,12 @@ class LDAPSource(Source):
return LDAPSourceForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.sources.ldap.api import LDAPSourceSerializer
return LDAPSourceSerializer
def state_cache_prefix(self, suffix: str) -> str:
"""Key by which the ldap source status is saved"""
return f"source_ldap_{self.pk}_state_{suffix}"

View file

@ -5,6 +5,7 @@ from django.db import models
from django.forms import ModelForm
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Source, UserSourceConnection
from authentik.core.types import UILoginButton
@ -46,6 +47,12 @@ class OAuthSource(Source):
return OAuthSourceForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.sources.oauth.api import OAuthSourceSerializer
return OAuthSourceSerializer
@property
def ui_login_button(self) -> UILoginButton:
return UILoginButton(

View file

@ -7,6 +7,7 @@ from django.http import HttpRequest
from django.shortcuts import reverse
from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _
from rest_framework.serializers import Serializer
from authentik.core.models import Source
from authentik.core.types import UILoginButton
@ -143,6 +144,12 @@ class SAMLSource(Source):
return SAMLSourceForm
@property
def serializer(self) -> Type[Serializer]:
from authentik.sources.saml.api import SAMLSourceSerializer
return SAMLSourceSerializer
def get_issuer(self, request: HttpRequest) -> str:
"""Get Source's Issuer, falling back to our Metadata URL if none is set"""
if self.issuer is None:

View file

@ -1129,7 +1129,7 @@ paths:
tags:
- flows
parameters: []
/flows/instances/{flow_uuid}/:
/flows/instances/{slug}/:
get:
operationId: flows_instances_read
description: Flow Viewset
@ -1183,12 +1183,13 @@ paths:
tags:
- flows
parameters:
- name: flow_uuid
- name: slug
in: path
description: A UUID string identifying this Flow.
description: Visible in the URL.
required: true
type: string
format: uuid
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/outposts/outposts/:
get:
operationId: outposts_outposts_list
@ -3788,7 +3789,7 @@ paths:
tags:
- sources
parameters: []
/sources/all/{pbm_uuid}/:
/sources/all/{slug}/:
get:
operationId: sources_all_read
description: Source Viewset
@ -3801,12 +3802,13 @@ paths:
tags:
- sources
parameters:
- name: pbm_uuid
- name: slug
in: path
description: A UUID string identifying this source.
description: Internal source name, used in URLs.
required: true
type: string
format: uuid
format: slug
pattern: ^[-a-zA-Z0-9_]+$
/sources/ldap/:
get:
operationId: sources_ldap_list
@ -6749,6 +6751,30 @@ definitions:
description: Optional Private Key. If this is set, you can use this keypair
for encryption.
type: string
Stage:
title: Stage obj
description: Stage Serializer
required:
- name
type: object
properties:
pk:
title: Stage uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
__type__:
title: 'type '
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
FlowStageBinding:
description: FlowStageBinding Serializer
required:
@ -6762,6 +6788,10 @@ definitions:
type: string
format: uuid
readOnly: true
policybindingmodel_ptr_id:
title: Policybindingmodel ptr id
type: string
readOnly: true
target:
title: Target
type: string
@ -6770,6 +6800,8 @@ definitions:
title: Stage
type: string
format: uuid
stage_obj:
$ref: '#/definitions/Stage'
evaluate_on_plan:
title: Evaluate on plan
description: Evaluate policies during the Flow planning process. Disable this
@ -6805,6 +6837,10 @@ definitions:
type: string
format: uuid
readOnly: true
policybindingmodel_ptr_id:
title: Policybindingmodel ptr id
type: string
readOnly: true
name:
title: Name
type: string
@ -8093,29 +8129,6 @@ definitions:
\ log out manually. (Format: hours=1;minutes=2;seconds=3)."
type: string
minLength: 1
Stage:
description: Stage Serializer
required:
- name
type: object
properties:
pk:
title: Stage uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
minLength: 1
__type__:
title: 'type '
type: string
readOnly: true
verbose_name:
title: Verbose name
type: string
readOnly: true
CaptchaStage:
description: CaptchaStage Serializer
required:

76
web/src/api/flow.ts Normal file
View file

@ -0,0 +1,76 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export enum FlowDesignation {
Authentication = "authentication",
Authorization = "authorization",
Invalidation = "invalidation",
Enrollment = "enrollment",
Unrenollment = "unenrollment",
Recovery = "recovery",
StageConfiguration = "stage_configuration",
}
export class Flow {
pk: string;
policybindingmodel_ptr_id: string;
name: string;
slug: string;
title: string;
designation: FlowDesignation;
background: string;
stages: string[];
policies: string[];
cache_count: number;
constructor() {
throw Error();
}
static get(slug: string): Promise<Flow> {
return DefaultClient.fetch<Flow>(["flows", "instances", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Flow>> {
return DefaultClient.fetch<PBResponse<Flow>>(["flows", "instances"], filter);
}
}
export class Stage {
pk: string;
name: string;
__type__: string;
verbose_name: string;
constructor() {
throw Error();
}
}
export class FlowStageBinding {
pk: string;
policybindingmodel_ptr_id: string;
target: string;
stage: string;
stage_obj: Stage;
evaluate_on_plan: boolean;
re_evaluate_policies: boolean;
order: number;
policies: string[];
constructor() {
throw Error();
}
static get(slug: string): Promise<FlowStageBinding> {
return DefaultClient.fetch<FlowStageBinding>(["flows", "bindings", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<FlowStageBinding>> {
return DefaultClient.fetch<PBResponse<FlowStageBinding>>(["flows", "bindings"], filter);
}
static adminUrl(rest: string): string {
return `/administration/stages/bindings/${rest}`;
}
}

View file

@ -1,15 +1,33 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export interface Policy {
pk: string;
name: string;
[key: string]: unknown;
}
export interface PolicyBinding {
export class PolicyBinding {
pk: string;
policy: string,
policy: string;
policy_obj: Policy;
target: string;
enabled: boolean;
order: number;
timeout: number;
constructor() {
throw Error();
}
static get(pk: string): Promise<PolicyBinding> {
return DefaultClient.fetch<PolicyBinding>(["policies", "bindings", pk]);
}
static list(filter?: QueryArguments): Promise<PBResponse<PolicyBinding>> {
return DefaultClient.fetch<PBResponse<PolicyBinding>>(["policies", "bindings"], filter);
}
static adminUrl(rest: string): string {
return `/administration/policies/bindings/${rest}`;
}
}

22
web/src/api/source.ts Normal file
View file

@ -0,0 +1,22 @@
import { DefaultClient, PBResponse, QueryArguments } from "./client";
export class Source {
pk: string;
name: string;
slug: string;
enabled: boolean;
authentication_flow: string;
enrollment_flow: string;
constructor() {
throw Error();
}
static get(slug: string): Promise<Source> {
return DefaultClient.fetch<Source>(["sources", "all", slug]);
}
static list(filter?: QueryArguments): Promise<PBResponse<Source>> {
return DefaultClient.fetch<PBResponse<Source>>(["sources", "all"], filter);
}
}

View file

@ -137,6 +137,14 @@ select[multiple] {
--pf-c-table--BorderColor: var(--ak-dark-background-lighter);
--pf-c-table--cell--Color: var(--ak-dark-foreground);
}
/* class for pagination text */
.pf-c-options-menu__toggle {
color: var(--ak-dark-foreground);
}
/* table icon used for expanding rows */
.pf-c-table__toggle-icon {
color: var(--ak-dark-foreground);
}
/* inputs */
.pf-c-form-control {
--pf-c-form-control--BorderTopColor: var(--ak-dark-background-lighter);
@ -151,6 +159,13 @@ select[multiple] {
background-color: var(--ak-dark-background-light);
color: var(--ak-dark-foreground);
}
.pf-c-button.pf-m-tertiary {
--pf-c-button--after--BorderColor: var(--ak-dark-foreground-darker);
color: var(--ak-dark-foreground-darker);
}
.pf-c-button.pf-m-tertiary:hover {
--pf-c-button--after--BorderColor: var(--ak-dark-background-lighter);
}
.pf-c-form__label-text {
color: var(--ak-dark-foreground);
}
@ -162,6 +177,12 @@ select[multiple] {
color: var(--ak-dark-foreground);
}
/* modal */
.pf-c-modal-box__header {
background-color: var(--ak-dark-background-light);
}
.pf-c-modal-box__body {
background-color: var(--ak-dark-background-light);
}
.pf-c-modal-box__footer {
background-color: var(--ak-dark-background-light);
}

View file

@ -0,0 +1,34 @@
import { CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../common/styles";
@customElement("ak-empty-state")
export class EmptyState extends LitElement {
@property({type: String})
icon = "";
@property()
header?: string;
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
return html`<div class="pf-c-empty-state">
<div class="pf-c-empty-state__content">
<i class="pf-icon ${this.icon} pf-c-empty-state__icon" aria-hidden="true"></i>
<h1 class="pf-c-title pf-m-lg">
${this.header}
</h1>
<div class="pf-c-empty-state__body">
<slot name="body"></slot>
</div>
<div class="pf-c-empty-state__primary">
<slot name="primary"></slot>
</div>
</div>
</div>`;
}
}

View file

@ -104,6 +104,7 @@ export class ModalButton extends LitElement {
this.dispatchEvent(
new CustomEvent("ak-refresh", {
bubbles: true,
composed: true,
})
);
}

View file

@ -0,0 +1,79 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { PolicyBinding } from "../../api/policy_binding";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
@customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> {
@property()
target?: string;
apiEndpoint(page: number): Promise<PBResponse<PolicyBinding>> {
return PolicyBinding.list({
target: this.target || "",
ordering: "order",
page: page,
});
}
columns(): string[] {
return ["Policy", "Enabled", "Order", "Timeout", ""];
}
row(item: PolicyBinding): TemplateResult[] {
return [
html`${item.policy_obj.name}`,
html`${item.enabled ? "Yes" : "No"}`,
html`${item.order}`,
html`${item.timeout}`,
html`
<ak-modal-button href="${PolicyBinding.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
Edit
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${PolicyBinding.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
Delete
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${gettext("No Policies bound.")} icon="pf-icon-module">
<div slot="body">
${gettext("No policies are currently bound to this object.")}
</div>
<div slot="primary">
<ak-modal-button href=${PolicyBinding.adminUrl(`create/?target=${this.target}`)}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Policy")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</ak-empty-state>`);
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href=${PolicyBinding.adminUrl(`create/?target=${this.target}`)}>
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Policy")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View file

@ -9,7 +9,7 @@ import { Route } from "./Route";
import { ROUTES } from "../../routes";
import { RouteMatch } from "./RouteMatch";
import "../generic/SiteShell";
import "../../pages/generic/SiteShell";
@customElement("ak-router-outlet")
export class RouterOutlet extends LitElement {

View file

@ -13,11 +13,58 @@ import { until } from "lit-html/directives/until";
import "./SidebarBrand";
import "./SidebarUser";
export interface SidebarItem {
export class SidebarItem {
name: string;
path?: string[];
children?: SidebarItem[];
condition?: () => Promise<boolean>;
path?: string;
_children: SidebarItem[];
condition: () => Promise<boolean>;
activeMatchers: RegExp[];
constructor(name: string, path?: string) {
this.name = name;
this.path = path;
this._children = [];
this.condition = async () => true;
this.activeMatchers = [];
if (this.path) {
this.activeMatchers.push(new RegExp(`^${this.path}$`));
}
}
children(...children: SidebarItem[]): SidebarItem {
this._children = children;
return this;
}
activeWhen(...regexp: string[]): SidebarItem {
regexp.forEach(r => {
this.activeMatchers.push(new RegExp(r));
});
return this;
}
when(condition: () => Promise<boolean>): SidebarItem {
this.condition = condition;
return this;
}
hasChildren(): boolean {
return this._children.length > 0;
}
isActive(activePath: string): boolean {
if (!this.path) {
return false;
}
return this.activeMatchers.some(v => {
const match = v.exec(activePath);
if (match !== null) {
return true;
}
});
}
}
@customElement("ak-sidebar")
@ -78,9 +125,9 @@ export class Sidebar extends LitElement {
return html``;
}
}
return html` <li class="pf-c-nav__item ${item.children ? "pf-m-expandable pf-m-expanded" : ""}">
return html` <li class="pf-c-nav__item ${item.hasChildren() ? "pf-m-expandable pf-m-expanded" : ""}">
${item.path ?
html`<a href="#${item.path}" class="pf-c-nav__link ${item.path.some((v) => v === this.activePath) ? "pf-m-current": ""}">
html`<a href="#${item.path}" class="pf-c-nav__link ${item.isActive(this.activePath) ? "pf-m-current": ""}">
${item.name}
</a>` :
html`<a class="pf-c-nav__link" aria-expanded="true">
@ -91,7 +138,7 @@ export class Sidebar extends LitElement {
</a>
<section class="pf-c-nav__subnav">
<ul class="pf-c-nav__simple-list">
${item.children?.map((i) => until(this.renderItem(i), html``))}
${item._children.map((i) => until(this.renderItem(i), html``))}
</ul>
</section>`}
</li>`;

View file

@ -2,14 +2,22 @@ import { gettext } from "django";
import { CSSResult, html, LitElement, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { COMMON_STYLES } from "../../common/styles";
import { htmlFromString } from "../../utils";
import "./TablePagination";
import "../EmptyState";
export abstract class Table<T> extends LitElement {
abstract apiEndpoint(page: number): Promise<PBResponse<T>>;
abstract columns(): Array<string>;
abstract row(item: T): Array<string>;
abstract row(item: T): Array<TemplateResult>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
renderExpanded(item: T): TemplateResult {
if (this.expandable) {
throw new Error("Expandable is enabled but renderExpanded is not overridden!");
}
return html``;
}
@property({attribute: false})
data?: PBResponse<T>;
@ -17,6 +25,12 @@ export abstract class Table<T> extends LitElement {
@property({type: Number})
page = 1;
@property({type: Boolean})
expandable = false;
@property({attribute: false})
expandedRows: boolean[] = [];
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
@ -48,7 +62,7 @@ export abstract class Table<T> extends LitElement {
<span class="pf-c-spinner__tail-ball"></span>
</span>
</div>
<h2 class="pf-c-title pf-m-lg">Loading</h2>
<h2 class="pf-c-title pf-m-lg">${gettext("Loading")}</h2>
</div>
</div>
</div>
@ -56,21 +70,59 @@ export abstract class Table<T> extends LitElement {
</tr>`;
}
renderEmpty(inner?: TemplateResult): TemplateResult {
return html`<tbody role="rowgroup">
<tr role="row">
<td role="cell" colspan="8">
<div class="pf-l-bullseye">
${inner ? inner : html`<ak-empty-state header="none"></ak-empty-state>`}
</div>
</td>
</tr>
</tbody>`;
}
private renderRows(): TemplateResult[] | undefined {
if (!this.data) {
return;
}
return this.data.results.map((item) => {
const fullRow = ["<tr role=\"row\">"].concat(
this.row(item).map((col) => {
return `<td role="cell">${col}</td>`;
})
);
fullRow.push("</tr>");
return htmlFromString(...fullRow);
if (this.data.pagination.count === 0) {
return [this.renderEmpty()];
}
return this.data.results.map((item: T, idx: number) => {
if ((this.expandedRows.length - 1) < idx) {
this.expandedRows[idx] = false;
}
return html`<tbody role="rowgroup" class="${this.expandedRows[idx] ? "pf-m-expanded" : ""}">
<tr role="row">
${this.expandable ? html`<td class="pf-c-table__toggle" role="cell">
<button class="pf-c-button pf-m-plain ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" @click=${() => {
this.expandedRows[idx] = !this.expandedRows[idx];
this.requestUpdate();
}}>
<div class="pf-c-table__toggle-icon"> <i class="fas fa-angle-down" aria-hidden="true"></i> </div>
</button>
</td>` : html``}
${this.row(item).map((col) => {
return html`<td role="cell">${col}</td>`;
})}
</tr>
<tr class="pf-c-table__expandable-row ${this.expandedRows[idx] ? "pf-m-expanded" : ""}" role="row">
<td></td>
${this.renderExpanded(item)}
</tr>
</tbody>`;
});
}
renderToolbar(): TemplateResult {
return html`&nbsp;<button
@click=${() => { this.fetch(); }}
class="pf-c-button pf-m-primary">
${gettext("Refresh")}
</button>`;
}
renderTable(): TemplateResult {
if (!this.data) {
this.fetch();
@ -78,12 +130,7 @@ export abstract class Table<T> extends LitElement {
return html`<div class="pf-c-toolbar">
<div class="pf-c-toolbar__content">
<div class="pf-c-toolbar__bulk-select">
<slot name="create-button"></slot>
<button
@click=${() => {this.fetch();}}
class="pf-c-button pf-m-primary">
${gettext("Refresh")}
</button>
${this.renderToolbar()}
</div>
<ak-table-pagination
class="pf-c-toolbar__item pf-m-pagination"
@ -92,15 +139,14 @@ export abstract class Table<T> extends LitElement {
</ak-table-pagination>
</div>
</div>
<table class="pf-c-table pf-m-compact pf-m-grid-md">
<table class="pf-c-table pf-m-compact pf-m-grid-md pf-m-expandable">
<thead>
<tr role="row">
${this.expandable ? html`<td role="cell">` : html``}
${this.columns().map((col) => html`<th role="columnheader" scope="col">${gettext(col)}</th>`)}
</tr>
</thead>
<tbody role="rowgroup">
${this.data ? this.renderRows() : this.renderLoading()}
</tbody>
</table>
<div class="pf-c-pagination pf-m-bottom">
<ak-table-pagination

View file

@ -39,7 +39,7 @@
<script src="/static/dist/main.js" type="module"></script>
</head>
<body>
<ak-messages url="/api/v2beta/root/messages/"></ak-messages>
<ak-messages></ak-messages>
<div class="pf-c-page">
<a class="pf-c-skip-to-content pf-c-button pf-m-primary" href="#main-content"
>Skip to content</a

View file

@ -1,120 +1,45 @@
import { customElement } from "lit-element";
import { User } from "../api/user";
import { SidebarItem } from "../elements/sidebar/Sidebar";
import { SLUG_REGEX } from "../elements/router/Route";
import { Interface } from "./Interface";
export const SIDEBAR_ITEMS: SidebarItem[] = [
{
name: "Library",
path: ["/library/"],
},
{
name: "Monitor",
path: ["/audit/audit/"],
condition: (): Promise<boolean> => {
new SidebarItem("Library", "/library/"),
new SidebarItem("Monitor", "/audit/audit").when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
},
},
{
name: "Administration",
children: [
{
name: "Overview",
path: ["/administration/overview-ng/"],
},
{
name: "System Tasks",
path: ["/administration/tasks/"],
},
{
name: "Applications",
path: ["/administration/applications/"],
},
{
name: "Sources",
path: ["/administration/sources/"],
},
{
name: "Providers",
path: ["/administration/providers/"],
},
{
name: "User Management",
children: [
{
name: "User",
path: ["/administration/users/"],
},
{
name: "Groups",
path: ["/administration/groups/"],
},
],
},
{
name: "Outposts",
children: [
{
name: "Outposts",
path: ["/administration/outposts/"],
},
{
name: "Service Connections",
path: ["/administration/outposts/service_connections/"],
},
],
},
{
name: "Policies",
children: [
{
name: "Policies",
path: ["/administration/policies/"],
},
{
name: "Bindings",
path: ["/administration/policies/bindings/"],
},
],
},
{
name: "Property Mappings",
path: ["/administration/property-mappings/"],
},
{
name: "Flows",
children: [
{
name: "Flows",
path: ["/administration/flows/"],
},
{
name: "Stages",
path: ["/administration/stages/"],
},
{
name: "Prompts",
path: ["/administration/stages/prompts/"],
},
{
name: "Invitations",
path: ["/administration/stages/invitations/"],
},
],
},
{
name: "Certificates",
path: ["/administration/crypto/certificates/"],
},
{
name: "Tokens",
path: ["/administration/tokens/"],
},
],
condition: (): Promise<boolean> => {
}),
new SidebarItem("Administration").children(
new SidebarItem("Overview", "/administration/overview-ng/"),
new SidebarItem("System Tasks", "/administration/tasks/"),
new SidebarItem("Applications", "/administration/applications/").activeWhen(
`^/applications/(?<slug>${SLUG_REGEX})/$`
),
new SidebarItem("Sources", "/administration/sources/").activeWhen(
`^/sources/(?<slug>${SLUG_REGEX})/$`,
),
new SidebarItem("Providers", "/administration/providers/"),
new SidebarItem("Flows").children(
new SidebarItem("Flows", "/administration/flows/").activeWhen(`^/flows/(?<slug>${SLUG_REGEX})/$`),
new SidebarItem("Stages", "/administration/stages/"),
new SidebarItem("Prompts", "/administration/stages/prompts/"),
new SidebarItem("Invitations", "/administration/stages/invitations/"),
),
new SidebarItem("User Management").children(
new SidebarItem("User", "/administration/users/"),
new SidebarItem("Groups", "/administration/groups/")
),
new SidebarItem("Outposts").children(
new SidebarItem("Outposts", "/administration/outposts/"),
new SidebarItem("Service Connections", "/administration/outposts/service_connections/")
),
new SidebarItem("Policies", "/administration/policies/"),
new SidebarItem("Property Mappings", "/administration/property-mappings"),
new SidebarItem("Certificates", "/administration/crypto/certificates"),
new SidebarItem("Tokens", "/administration/tokens/"),
).when((): Promise<boolean> => {
return User.me().then(u => u.is_superuser);
},
},
})
];
@customElement("ak-interface-admin")

View file

@ -3,7 +3,7 @@ import { html, LitElement, TemplateResult } from "lit-element";
import { SidebarItem } from "../elements/sidebar/Sidebar";
import "../elements/Messages";
import "../pages/router/RouterOutlet";
import "../elements/router/RouterOutlet";
export abstract class Interface extends LitElement {

View file

@ -13,18 +13,18 @@ import "./elements/sidebar/SidebarUser";
import "./elements/table/TablePagination";
import "./elements/AdminLoginsChart";
import "./elements/EmptyState";
import "./elements/cards/AggregateCard";
import "./elements/cards/AggregatePromiseCard";
import "./elements/CodeMirror";
import "./elements/Messages";
import "./elements/Spinner";
import "./elements/Tabs";
import "./elements/router/RouterOutlet";
import "./pages/generic/FlowShellCard";
import "./pages/generic/SiteShell";
import "./pages/router/RouterOutlet";
import "./pages/admin-overview/AdminOverviewPage";
import "./pages/admin-overview/TopApplicationsTable";
import "./pages/applications/ApplicationListPage";

View file

@ -1,9 +1,12 @@
import { gettext } from "django";
import { customElement } from "lit-element";
import { customElement, html, TemplateResult } from "lit-element";
import { Application } from "../../api/application";
import { PBResponse } from "../../api/client";
import { TablePage } from "../../elements/table/TablePage";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
@customElement("ak-application-list")
export class ApplicationList extends TablePage<Application> {
pageTitle(): string {
@ -27,13 +30,13 @@ export class ApplicationList extends TablePage<Application> {
return ["Name", "Slug", "Provider", "Provider Type", ""];
}
row(item: Application): string[] {
row(item: Application): TemplateResult[] {
return [
item.name,
item.slug,
item.provider.toString(),
item.provider.toString(),
`
html`${item.name}`,
html`${item.slug}`,
html`${item.provider}`,
html`${item.provider}`,
html`
<ak-modal-button href="administration/policies/bindings/${item.pk}/update/">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
Edit

View file

@ -1,54 +1,14 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { Application } from "../../api/application";
import { DefaultClient, PBResponse } from "../../api/client";
import { PolicyBinding } from "../../api/policy_binding";
import { DefaultClient } from "../../api/client";
import { COMMON_STYLES } from "../../common/styles";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
@customElement("ak-bound-policies-list")
export class BoundPoliciesList extends Table<PolicyBinding> {
@property()
target?: string;
apiEndpoint(page: number): Promise<PBResponse<PolicyBinding>> {
return DefaultClient.fetch<PBResponse<PolicyBinding>>(["policies", "bindings"], {
target: this.target || "",
ordering: "order",
page: page,
});
}
columns(): string[] {
return ["Policy", "Enabled", "Order", "Timeout", ""];
}
row(item: PolicyBinding): string[] {
return [
item.policy_obj.name,
item.enabled ? "Yes" : "No",
item.order.toString(),
item.timeout.toString(),
`
<ak-modal-button href="administration/policies/bindings/${item.pk}/update/">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
Edit
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="administration/policies/bindings/${item.pk}/delete/">
<ak-spinner-button slot="trigger" class="pf-m-danger">
Delete
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
}
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
@customElement("ak-application-view")
export class ApplicationViewPage extends LitElement {
@ -108,7 +68,13 @@ export class ApplicationViewPage extends LitElement {
</section>
<div slot="page-2" data-tab-title="Policy Bindings" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<ak-bound-policies-list .target=${this.application.pk}></ak-bound-policies-list>
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
${gettext("These policies control which users can access this application.")}
</div>
</div>
<ak-bound-policies-list .target=${this.application.pk}>
</ak-bound-policies-list>
</div>
</div>
</ak-tabs>`;

View file

@ -0,0 +1,97 @@
import { gettext } from "django";
import { customElement, html, property, TemplateResult } from "lit-element";
import { PBResponse } from "../../api/client";
import { Table } from "../../elements/table/Table";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import { FlowStageBinding } from "../../api/flow";
@customElement("ak-bound-stages-list")
export class BoundStagesList extends Table<FlowStageBinding> {
expandable = true;
@property()
target?: string;
apiEndpoint(page: number): Promise<PBResponse<FlowStageBinding>> {
return FlowStageBinding.list({
target: this.target || "",
ordering: "order",
page: page,
});
}
columns(): string[] {
return ["Order", "Name", "Type", ""];
}
row(item: FlowStageBinding): TemplateResult[] {
return [
html`${item.order}`,
html`${item.stage_obj.name}`,
html`${item.stage_obj.verbose_name}`,
html`
<ak-modal-button href="${FlowStageBinding.adminUrl(`${item.pk}/update/`)}">
<ak-spinner-button slot="trigger" class="pf-m-secondary">
Edit
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
<ak-modal-button href="${FlowStageBinding.adminUrl(`${item.pk}/delete/`)}">
<ak-spinner-button slot="trigger" class="pf-m-danger">
Delete
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
`,
];
}
renderExpanded(item: FlowStageBinding): TemplateResult {
return html`
<td></td>
<td role="cell" colspan="3">
<div class="pf-c-table__expandable-row-content">
<div class="pf-c-content">
<p>${gettext("These policies control when this stage will be applied to the flow.")}</p>
<ak-bound-policies-list .target=${item.policybindingmodel_ptr_id}>
</ak-bound-policies-list>
</div>
</div>
</td>
<td></td>
<td></td>`;
}
renderEmpty(): TemplateResult {
return super.renderEmpty(html`<ak-empty-state header=${gettext("No Stages bound")} icon="pf-icon-module">
<div slot="body">
${gettext("No stages are currently bound to this flow.")}
</div>
<div slot="primary">
<ak-modal-button href="${FlowStageBinding.adminUrl(`create/?target=${this.target}`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Stage")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
</div>
</ak-empty-state>`);
}
renderToolbar(): TemplateResult {
return html`
<ak-modal-button href="${FlowStageBinding.adminUrl(`create/?target=${this.target}`)}">
<ak-spinner-button slot="trigger" class="pf-m-primary">
${gettext("Bind Stage")}
</ak-spinner-button>
<div slot="modal"></div>
</ak-modal-button>
${super.renderToolbar()}
`;
}
}

View file

@ -0,0 +1,71 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import { Flow } from "../../api/flow";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import "./BoundStagesList";
@customElement("ak-flow-view")
export class FlowViewPage extends LitElement {
@property()
set args(value: { [key: string]: string }) {
this.flowSlug = value.slug;
}
@property()
set flowSlug(value: string) {
Flow.get(value).then((flow) => (this.flow = flow));
}
@property({attribute: false})
flow?: Flow;
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
img.pf-icon {
max-height: 24px;
}
`
);
}
render(): TemplateResult {
if (!this.flow) {
return html``;
}
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-process-automation"></i>
${this.flow?.name}
</h1>
<p>${this.flow?.title}</p>
</div>
</section>
<ak-tabs>
<div slot="page-2" data-tab-title="${gettext("Stage Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<ak-bound-stages-list .target=${this.flow.pk}>
</ak-bound-stages-list>
</div>
</div>
<div slot="page-3" data-tab-title="${gettext("Policy Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
${gettext("These policies control which users can access this flow.")}
</div>
</div>
<ak-bound-policies-list .target=${this.flow.policybindingmodel_ptr_id}>
</ak-bound-policies-list>
</div>
</div>
</ak-tabs>`;
}
}

View file

@ -0,0 +1,63 @@
import { gettext } from "django";
import { css, CSSResult, customElement, html, LitElement, property, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import "../../elements/Tabs";
import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList";
import { Source } from "../../api/source";
@customElement("ak-source-view")
export class SourceViewPage extends LitElement {
@property()
set args(value: { [key: string]: string }) {
this.sourceSlug = value.slug;
}
@property()
set sourceSlug(value: string) {
Source.get(value).then((source) => (this.source = source));
}
@property({attribute: false})
source?: Source;
static get styles(): CSSResult[] {
return COMMON_STYLES.concat(
css`
img.pf-icon {
max-height: 24px;
}
`
);
}
render(): TemplateResult {
if (!this.source) {
return html``;
}
return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content">
<h1>
<i class="pf-icon pf-icon-middleware"></i>
${this.source?.name}
</h1>
</div>
</section>
<ak-tabs>
<div slot="page-2" data-tab-title="Policy Bindings" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-c-card">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
${gettext("These policies control which users can access this application.")}
</div>
</div>
<ak-bound-policies-list .target=${this.source.pk}>
</ak-bound-policies-list>
</div>
</div>
</ak-tabs>`;
}
}

View file

@ -1,10 +1,12 @@
import { html } from "lit-html";
import { Route, SLUG_REGEX } from "./pages/router/Route";
import { Route, SLUG_REGEX } from "./elements/router/Route";
import "./pages/LibraryPage";
import "./pages/admin-overview/AdminOverviewPage";
import "./pages/applications/ApplicationListPage";
import "./pages/applications/ApplicationViewPage";
import "./pages/sources/SourceViewPage";
import "./pages/flows/FlowViewPage";
export const ROUTES: Route[] = [
// Prevent infinite Shell loops
@ -16,4 +18,10 @@ export const ROUTES: Route[] = [
new Route(new RegExp(`^/applications/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-application-view .args=${args}></ak-application-view>`;
}),
new Route(new RegExp(`^/sources/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-source-view .args=${args}></ak-source-view>`;
}),
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})/$`)).then((args) => {
return html`<ak-flow-view .args=${args}></ak-flow-view>`;
}),
];