web: add more related links, add policy/user/group support for bindings

This commit is contained in:
Jens Langhammer 2021-02-16 20:34:15 +01:00
parent 6bcdf36ca6
commit f8ba623fc1
12 changed files with 398 additions and 48 deletions

View File

@ -11,7 +11,7 @@ from authentik.lib.sentry import SentryIgnoredException
def get_attrs(obj: SerializerModel) -> Dict[str, Any]: def get_attrs(obj: SerializerModel) -> Dict[str, Any]:
"""Get object's attributes via their serializer, and covert it to a normal dict""" """Get object's attributes via their serializer, and covert it to a normal dict"""
data = dict(obj.serializer(obj).data) data = dict(obj.serializer(obj).data)
to_remove = ("policies", "stages", "pk", "background") to_remove = ("policies", "stages", "pk", "background", "group", "user")
for to_remove_name in to_remove: for to_remove_name in to_remove:
if to_remove_name in data: if to_remove_name in data:
data.pop(to_remove_name) data.pop(to_remove_name)

View File

@ -82,6 +82,8 @@ class FlowImporter:
main_query = Q(pk=attrs["pk"]) main_query = Q(pk=attrs["pk"])
sub_query = Q() sub_query = Q()
for identifier, value in attrs.items(): for identifier, value in attrs.items():
if isinstance(value, dict):
continue
if identifier == "pk": if identifier == "pk":
continue continue
sub_query &= Q(**{identifier: value}) sub_query &= Q(**{identifier: value})

View File

@ -99,15 +99,12 @@ class PolicyBindingSerializer(ModelSerializer):
required=True, required=True,
) )
policy_obj = PolicySerializer(read_only=True, source="policy")
class Meta: class Meta:
model = PolicyBinding model = PolicyBinding
fields = [ fields = [
"pk", "pk",
"policy", "policy",
"policy_obj",
"group", "group",
"user", "user",
"target", "target",
@ -115,12 +112,13 @@ class PolicyBindingSerializer(ModelSerializer):
"order", "order",
"timeout", "timeout",
] ]
depth = 2
class PolicyBindingViewSet(ModelViewSet): class PolicyBindingViewSet(ModelViewSet):
"""PolicyBinding Viewset""" """PolicyBinding Viewset"""
queryset = PolicyBinding.objects.all() queryset = PolicyBinding.objects.all().select_related("policy", "target", "group", "user")
serializer_class = PolicyBindingSerializer serializer_class = PolicyBindingSerializer
filterset_fields = ["policy", "target", "enabled", "order", "timeout"] filterset_fields = ["policy", "target", "enabled", "order", "timeout"]
search_fields = ["policy__name"] search_fields = ["policy__name"]

View File

@ -8643,21 +8643,327 @@ definitions:
format: uuid format: uuid
readOnly: true readOnly: true
policy: policy:
title: Policy description: Policies which specify if a user is authorized to use an Application.
type: string Can be overridden by other types to add other fields, more logic, etc.
format: uuid type: object
x-nullable: true properties:
policy_obj: policy_uuid:
$ref: '#/definitions/Policy' title: Policy uuid
type: string
format: uuid
readOnly: true
created:
title: Created
type: string
format: date-time
readOnly: true
last_updated:
title: Last updated
type: string
format: date-time
readOnly: true
name:
title: Name
type: string
x-nullable: true
execution_logging:
title: Execution logging
description: When this option is enabled, all executions of this policy
will be logged. By default, only execution errors are logged.
type: boolean
readOnly: true
group: group:
title: Group description: Custom Group model which supports a basic hierarchy
type: string required:
format: uuid - name
x-nullable: true type: object
properties:
group_uuid:
title: Group uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
maxLength: 80
minLength: 1
is_superuser:
title: Is superuser
description: Users added to this group will be superusers.
type: boolean
attributes:
title: Attributes
type: object
parent:
description: Custom Group model which supports a basic hierarchy
required:
- name
- parent
type: object
properties:
group_uuid:
title: Group uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
maxLength: 80
minLength: 1
is_superuser:
title: Is superuser
description: Users added to this group will be superusers.
type: boolean
attributes:
title: Attributes
type: object
parent:
title: Parent
type: string
format: uuid
readOnly: true
readOnly: true
user: user:
title: User description: Custom User model to allow easier adding o f user-based settings
type: integer required:
x-nullable: true - password
- username
- name
type: object
properties:
id:
title: ID
type: integer
readOnly: true
password:
title: Password
type: string
maxLength: 128
minLength: 1
last_login:
title: Last login
type: string
format: date-time
x-nullable: true
username:
title: Username
description: Required. 150 characters or fewer. Letters, digits and @/./+/-/_
only.
type: string
pattern: ^[\w.@+-]+$
maxLength: 150
minLength: 1
first_name:
title: First name
type: string
maxLength: 150
last_name:
title: Last name
type: string
maxLength: 150
email:
title: Email address
type: string
format: email
maxLength: 254
is_active:
title: Active
description: Designates whether this user should be treated as active.
Unselect this instead of deleting accounts.
type: boolean
date_joined:
title: Date joined
type: string
format: date-time
uuid:
title: Uuid
type: string
format: uuid
readOnly: true
name:
title: Name
description: User's display name.
type: string
minLength: 1
password_change_date:
title: Password change date
type: string
format: date-time
readOnly: true
attributes:
title: Attributes
type: object
groups:
description: ''
type: array
items:
description: Groups are a generic way of categorizing users to apply
permissions, or some other label, to those users. A user can belong
to any number of groups. A user in a group automatically has all the
permissions granted to that group. For example, if the group 'Site
editors' has the permission can_edit_home_page, any user in that group
will have that permission. Beyond permissions, groups are a convenient
way to categorize users to apply some label, or extended functionality,
to them. For example, you could create a group 'Special users', and
you could write code that would do special things to those users --
such as giving them access to a members-only portion of your site,
or sending them members-only email messages.
required:
- name
type: object
properties:
id:
title: ID
type: integer
readOnly: true
name:
title: Name
type: string
maxLength: 150
minLength: 1
permissions:
type: array
items:
type: integer
uniqueItems: true
readOnly: true
user_permissions:
description: ''
type: array
items:
description: "The permissions system provides a way to assign permissions\
\ to specific users and groups of users. The permission system is\
\ used by the Django admin site, but may also be useful in your own\
\ code. The Django admin site uses permissions as follows: - The \"\
add\" permission limits the user's ability to view the \"add\" form\
\ and add an object. - The \"change\" permission limits a user's ability\
\ to view the change list, view the \"change\" form and change an\
\ object. - The \"delete\" permission limits the ability to delete\
\ an object. - The \"view\" permission limits the ability to view\
\ an object. Permissions are set globally per type of object, not\
\ per specific object instance. It is possible to say \"Mary may change\
\ news stories,\" but it's not currently possible to say \"Mary may\
\ change news stories, but only the ones she created herself\" or\
\ \"Mary may only change news stories that have a certain status or\
\ publication date.\" The permissions listed above are automatically\
\ created for each model."
required:
- name
- codename
- content_type
type: object
properties:
id:
title: ID
type: integer
readOnly: true
name:
title: Name
type: string
maxLength: 255
minLength: 1
codename:
title: Codename
type: string
maxLength: 100
minLength: 1
content_type:
title: Content type
type: integer
readOnly: true
sources:
description: ''
type: array
items:
description: Base Authentication source, i.e. an OAuth Provider, SAML
Remote or LDAP Server
required:
- name
- slug
type: object
properties:
pbm_uuid:
title: Pbm uuid
type: string
format: uuid
readOnly: true
name:
title: Name
description: Source's display Name.
type: string
minLength: 1
slug:
title: Slug
description: Internal source name, used in URLs.
type: string
format: slug
pattern: ^[-a-zA-Z0-9_]+$
maxLength: 50
minLength: 1
enabled:
title: Enabled
type: boolean
authentication_flow:
title: Authentication flow
description: Flow to use when authenticating existing users.
type: string
format: uuid
x-nullable: true
enrollment_flow:
title: Enrollment flow
description: Flow to use when enrolling new users.
type: string
format: uuid
x-nullable: true
policies:
type: array
items:
type: string
format: uuid
readOnly: true
uniqueItems: true
property_mappings:
type: array
items:
type: string
format: uuid
uniqueItems: true
readOnly: true
ak_groups:
description: ''
type: array
items:
description: Custom Group model which supports a basic hierarchy
required:
- name
- parent
type: object
properties:
group_uuid:
title: Group uuid
type: string
format: uuid
readOnly: true
name:
title: Name
type: string
maxLength: 80
minLength: 1
is_superuser:
title: Is superuser
description: Users added to this group will be superusers.
type: boolean
attributes:
title: Attributes
type: object
parent:
title: Parent
type: string
format: uuid
readOnly: true
readOnly: true
target: target:
title: Target title: Target
type: string type: string

View File

@ -1,10 +1,14 @@
import { DefaultClient, AKResponse, QueryArguments } from "./Client"; import { DefaultClient, AKResponse, QueryArguments } from "./Client";
import { Group } from "./Groups";
import { Policy } from "./Policies"; import { Policy } from "./Policies";
import { User } from "./Users";
export class PolicyBinding { export class PolicyBinding {
pk: string; pk: string;
policy: string; policy: string;
policy_obj: Policy; policy_obj?: Policy;
group?: Group;
user?: User;
target: string; target: string;
enabled: boolean; enabled: boolean;
order: number; order: number;

View File

@ -24,7 +24,7 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
columns(): TableColumn[] { columns(): TableColumn[] {
return [ return [
new TableColumn("Policy"), new TableColumn("Policy / User / Group"),
new TableColumn("Enabled", "enabled"), new TableColumn("Enabled", "enabled"),
new TableColumn("Order", "order"), new TableColumn("Order", "order"),
new TableColumn("Timeout", "timeout"), new TableColumn("Timeout", "timeout"),
@ -32,9 +32,21 @@ export class BoundPoliciesList extends Table<PolicyBinding> {
]; ];
} }
getPolicyUserGroupRow(item: PolicyBinding): string {
if (item.policy_obj) {
return gettext(`Policy ${item.policy_obj.name}`);
} else if (item.group) {
return gettext(`Group ${item.group.name}`);
} else if (item.user) {
return gettext(`User ${item.user.name}`);
} else {
return gettext(``);
}
}
row(item: PolicyBinding): TemplateResult[] { row(item: PolicyBinding): TemplateResult[] {
return [ return [
html`${item.policy_obj.name}`, html`${this.getPolicyUserGroupRow(item)}`,
html`${item.enabled ? "Yes" : "No"}`, html`${item.enabled ? "Yes" : "No"}`,
html`${item.order}`, html`${item.order}`,
html`${item.timeout}`, html`${item.timeout}`,

View File

@ -0,0 +1,24 @@
import { commands } from "codemirror";
import { CSSResult, customElement, html, LitElement, TemplateResult } from "lit-element";
import { COMMON_STYLES } from "../../common/styles";
import { SpinnerSize } from "../Spinner";
@customElement("ak-loading-state")
export class LoadingState extends LitElement {
static get styles(): CSSResult[] {
return COMMON_STYLES;
}
render(): TemplateResult {
return html`<div class="pf-c-empty-state pf-m-full-height">
<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${SpinnerSize.XLarge}"></ak-spinner>
</div>
</div>
</div>
</div>`;
}
}

View File

@ -9,6 +9,7 @@ import "../../elements/AdminLoginsChart";
import "../../elements/buttons/ModalButton"; import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import "../../elements/policies/BoundPoliciesList"; import "../../elements/policies/BoundPoliciesList";
import "../../elements/utils/LoadingState";
@customElement("ak-application-view") @customElement("ak-application-view")
export class ApplicationViewPage extends LitElement { export class ApplicationViewPage extends LitElement {
@ -37,7 +38,7 @@ export class ApplicationViewPage extends LitElement {
render(): TemplateResult { render(): TemplateResult {
if (!this.application) { if (!this.application) {
return html``; return html`<ak-loading-state></ak-loading-state>`;;
} }
return html`<section class="pf-c-page__main-section pf-m-light"> return html`<section class="pf-c-page__main-section pf-m-light">
<div class="pf-c-content"> <div class="pf-c-content">
@ -49,7 +50,7 @@ export class ApplicationViewPage extends LitElement {
</div> </div>
</section> </section>
<ak-tabs> <ak-tabs>
<section slot="page-1" data-tab-title="${gettext("Users")}" class="pf-c-page__main-section pf-m-no-padding-mobile"> <section slot="page-1" data-tab-title="${gettext("Overview")}" class="pf-c-page__main-section pf-m-no-padding-mobile">
<div class="pf-l-gallery pf-m-gutter"> <div class="pf-l-gallery pf-m-gutter">
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;"> <div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-4-col" style="grid-column-end: span 3;grid-row-end: span 2;">
<div class="pf-c-card__header"> <div class="pf-c-card__header">
@ -64,6 +65,31 @@ export class ApplicationViewPage extends LitElement {
</ak-admin-logins-chart>`: ""} </ak-admin-logins-chart>`: ""}
</div> </div>
</div> </div>
<div class="pf-c-card pf-c-card-aggregate pf-l-gallery__item pf-m-2-col">
<div class="pf-c-card__header">
<div class="pf-c-card__header-main">
<i class="fas fa-external-link-alt"></i> ${gettext("Related")}
</div>
</div>
<div class="pf-c-card__body">
<dl class="pf-c-description-list pf-m-horizontal">
${this.application.provider ?
html`<div class="pf-c-description-list__group">
<dt class="pf-c-description-list__term">
<span class="pf-c-description-list__text">${gettext("Provider")}</span>
</dt>
<dd class="pf-c-description-list__description">
<div class="pf-c-description-list__text">
<a href="#/providers/${this.application.provider.pk}">
${this.application.provider.name}
</a>
</div>
</dd>
</div>`:
html``}
</dl>
</div>
</div>
</div> </div>
</section> </section>
<div slot="page-2" data-tab-title="${gettext("Policy Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile"> <div slot="page-2" data-tab-title="${gettext("Policy Bindings")}" class="pf-c-page__main-section pf-m-no-padding-mobile">

View File

@ -13,11 +13,6 @@ import "./FlowDiagram";
@customElement("ak-flow-view") @customElement("ak-flow-view")
export class FlowViewPage extends LitElement { export class FlowViewPage extends LitElement {
@property()
set args(value: { [key: string]: string }) {
this.flowSlug = value.slug;
}
@property() @property()
set flowSlug(value: string) { set flowSlug(value: string) {
Flow.get(value).then((flow) => (this.flow = flow)); Flow.get(value).then((flow) => (this.flow = flow));

View File

@ -4,7 +4,6 @@ import { COMMON_STYLES } from "../../common/styles";
import "../../elements/buttons/ModalButton"; import "../../elements/buttons/ModalButton";
import "../../elements/buttons/SpinnerButton"; import "../../elements/buttons/SpinnerButton";
import { SpinnerSize } from "../../elements/Spinner";
import "./SAMLProviderViewPage"; import "./SAMLProviderViewPage";
import "./OAuth2ProviderViewPage"; import "./OAuth2ProviderViewPage";
@ -27,15 +26,7 @@ export class ProviderViewPage extends LitElement {
render(): TemplateResult { render(): TemplateResult {
if (!this.provider) { if (!this.provider) {
return html`<div class="pf-c-empty-state pf-m-full-height"> return html`<ak-loading-state></ak-loading-state>`;;
<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${SpinnerSize.XLarge}"></ak-spinner>
</div>
</div>
</div>
</div>`;
} }
switch (this.provider?.object_type) { switch (this.provider?.object_type) {
case "saml": case "saml":

View File

@ -36,7 +36,7 @@ export const ROUTES: Route[] = [
return html`<ak-source-view .args=${args}></ak-source-view>`; return html`<ak-source-view .args=${args}></ak-source-view>`;
}), }),
new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => { new Route(new RegExp(`^/flows/(?<slug>${SLUG_REGEX})$`)).then((args) => {
return html`<ak-flow-view .args=${args}></ak-flow-view>`; return html`<ak-flow-view .flowSlug=${args.slug}></ak-flow-view>`;
}), }),
new Route(new RegExp("^/events/log$"), html`<ak-event-list></ak-event-list>`), new Route(new RegExp("^/events/log$"), html`<ak-event-list></ak-event-list>`),
new Route(new RegExp(`^/events/log/(?<id>${UUID_REGEX})$`)).then((args) => { new Route(new RegExp(`^/events/log/(?<id>${UUID_REGEX})$`)).then((args) => {

View File

@ -47,15 +47,7 @@ export function htmlFromString(...strings: string[]): TemplateResult {
export function loading<T>(v: T, actual: TemplateResult): TemplateResult { export function loading<T>(v: T, actual: TemplateResult): TemplateResult {
if (!v) { if (!v) {
return html`<div class="pf-c-empty-state pf-m-full-height"> return html`<ak-loading-state></ak-loading-state>`;
<div class="pf-c-empty-state__content">
<div class="pf-l-bullseye">
<div class="pf-l-bullseye__item">
<ak-spinner size="${SpinnerSize.XLarge}"></ak-spinner>
</div>
</div>
</div>
</div>`;
} }
return actual; return actual;
} }