
* tenants -> brands, init new tenant model, migrate some config to tenants Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * setup logging for tenants Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * configure celery and cache Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * small fixes, runs Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * task fixes, creation of tenant now works by cloning a template schema, some other small stuff Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix-tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * upstream fixes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix-pylint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix avatar tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * migrate config reputation_expiry as well Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix web rebase Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations for template schema Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations for template schema Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix migrations for template schema 3 Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * revert reputation expiry migration Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix type Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix some more tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * website: tenants -> brands Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * try fixing e2e tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * start frontend :help: Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add ability to disable tenants api Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * delete embedded outpost if it is disabled Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * make sure embedded outpost is disabled when tenants are enabled Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * management commands: add --schema option where relevant Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * store files per-tenant Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix embedded outpost deletion Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix files migration Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add tenant api tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add domain tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add settings tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * make --schema-name default to public in mgmt commands Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * sources/ldap: make sure lock is per-tenant Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix stuff I broke Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix remaining failing tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * try fixing e2e tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * much better frontend, but save does not refresh form properly Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * update django-tenants with latest fixes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * i18n-extract Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * review comments Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * move event_retention from brands to tenants Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * wip Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * root: add support for storing media files in S3 Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * use permissions for settings api Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * blueprints: disable tenants management Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix embedded outpost create/delete logic Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * make gen Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * make sure prometheus metrics are correctly served Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * makefile: don't delete the go api client when not regenerating it Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * tenants api: add recovery group and token creation endpoints Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix startup Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix prometheus metrics Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix web stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix migrations from stable Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix oauth source type import Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Revert "fix oauth source type import" This reverts commitd015fd0244
. * try with setting_changed signal Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * try with connection_created signal Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix scim tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix web after merge Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix enterprise settings Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * Revert "try with connection_created signal" This reverts commit764a999db8
. * Revert "try with setting_changed signal" This reverts commit32b40a3bbb
. * lib/expression: refactor expression compilation Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix django version Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix web after merge Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * relock poetry Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix reconcile Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * try running tenant save in a transaction Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * black Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * test: export postgres logs for debugging and use failfast Signed-off-by: Jens Langhammer <jens@goauthentik.io> * test: fix container name for logs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * do not copy tenant data Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * Revert "try running tenant save in a transaction" This reverts commitda6dec5a61
. * Revert "do not copy tenant data" This reverts commit d07ae9423672f068b0bd8be409ff9b58452a80f2. * Revert "Revert "do not copy tenant data"" This reverts commit4bffb19704
. * fix clone with nodata Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * why not Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * remove failfast Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove postgres query logging Signed-off-by: Jens Langhammer <jens@goauthentik.io> * update reconcile logic to clearly differentiate between tenant and global Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix reconcile app decorator Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * enable django checks Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * actually nodata was unnecessary as we're cloning from template and not from public Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * pylint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * update django-tenants with sequence fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * actually update Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix e2e tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add tests for settings api Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * add tests for recovery api Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * recovery tests: do them on a new tenant Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * web: fix system status being degraded when embedded outpost is disabled Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix recovery tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tenants tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint-fix Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * update UI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add management command to create a tenant Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add docs Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * release notes Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * more docs Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * checklist Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * self review Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * spelling Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * make web after upgrading Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * remove extra xlif file Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * prettier Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * Revert "add management command to create a tenant" This reverts commit39d13c0447
. * split api into smaller files, only import urls when tenants is enabled Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rewite some things on the release notes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * root: make sure install_id comes from public schema Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * require a license to use tenants Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * lint Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix tenants tests Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * fix files migration Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * release notes: add warning about user sessions being invalidated Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> * remove api disabled test, we can't test for it Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> --------- Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space> Signed-off-by: Jens Langhammer <jens@goauthentik.io> Co-authored-by: Jens Langhammer <jens@goauthentik.io>
263 lines
12 KiB
TypeScript
263 lines
12 KiB
TypeScript
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
|
import { BaseProviderForm } from "@goauthentik/admin/providers/BaseProviderForm";
|
|
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
|
import { first } from "@goauthentik/common/utils";
|
|
import { WithBrandConfig } from "@goauthentik/elements/Interface/brandProvider";
|
|
import "@goauthentik/elements/forms/FormGroup";
|
|
import "@goauthentik/elements/forms/HorizontalFormElement";
|
|
import "@goauthentik/elements/forms/Radio";
|
|
import "@goauthentik/elements/forms/SearchSelect";
|
|
|
|
import { msg } from "@lit/localize";
|
|
import { TemplateResult, html } from "lit";
|
|
import { customElement } from "lit/decorators.js";
|
|
import { ifDefined } from "lit/directives/if-defined.js";
|
|
|
|
import {
|
|
CoreApi,
|
|
CoreGroupsListRequest,
|
|
FlowsInstancesListDesignationEnum,
|
|
Group,
|
|
LDAPAPIAccessMode,
|
|
LDAPProvider,
|
|
ProvidersApi,
|
|
} from "@goauthentik/api";
|
|
|
|
@customElement("ak-provider-ldap-form")
|
|
export class LDAPProviderFormPage extends WithBrandConfig(BaseProviderForm<LDAPProvider>) {
|
|
async loadInstance(pk: number): Promise<LDAPProvider> {
|
|
return new ProvidersApi(DEFAULT_CONFIG).providersLdapRetrieve({
|
|
id: pk,
|
|
});
|
|
}
|
|
|
|
async send(data: LDAPProvider): Promise<LDAPProvider> {
|
|
if (this.instance) {
|
|
return new ProvidersApi(DEFAULT_CONFIG).providersLdapUpdate({
|
|
id: this.instance.pk || 0,
|
|
lDAPProviderRequest: data,
|
|
});
|
|
} else {
|
|
return new ProvidersApi(DEFAULT_CONFIG).providersLdapCreate({
|
|
lDAPProviderRequest: data,
|
|
});
|
|
}
|
|
}
|
|
|
|
// All Provider objects have an Authorization flow, but not all providers have an Authentication
|
|
// flow. LDAP needs only one field, but it is not an Authorization field, it is an
|
|
// Authentication field. So, yeah, we're using the authorization field to store the
|
|
// authentication information, which is why the ak-branded-flow-search call down there looks so
|
|
// weird-- we're looking up Authentication flows, but we're storing them in the Authorization
|
|
// field of the target Provider.
|
|
renderForm(): TemplateResult {
|
|
return html` <ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
|
|
<input
|
|
type="text"
|
|
value="${ifDefined(this.instance?.name)}"
|
|
class="pf-c-form-control"
|
|
required
|
|
/>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal
|
|
label=${msg("Bind flow")}
|
|
?required=${true}
|
|
name="authorizationFlow"
|
|
>
|
|
<ak-branded-flow-search
|
|
flowType=${FlowsInstancesListDesignationEnum.Authentication}
|
|
.currentFlow=${this.instance?.authorizationFlow}
|
|
.brandFlow=${this.brand?.flowAuthentication}
|
|
required
|
|
></ak-branded-flow-search>
|
|
<p class="pf-c-form__helper-text">${msg("Flow used for users to authenticate.")}</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal label=${msg("Search group")} name="searchGroup">
|
|
<ak-search-select
|
|
.fetchObjects=${async (query?: string): Promise<Group[]> => {
|
|
const args: CoreGroupsListRequest = {
|
|
ordering: "name",
|
|
};
|
|
if (query !== undefined) {
|
|
args.search = query;
|
|
}
|
|
const groups = await new CoreApi(DEFAULT_CONFIG).coreGroupsList(args);
|
|
return groups.results;
|
|
}}
|
|
.renderElement=${(group: Group): string => {
|
|
return group.name;
|
|
}}
|
|
.value=${(group: Group | undefined): string | undefined => {
|
|
return group?.pk;
|
|
}}
|
|
.selected=${(group: Group): boolean => {
|
|
return group.pk === this.instance?.searchGroup;
|
|
}}
|
|
?blankable=${true}
|
|
>
|
|
</ak-search-select>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"Users in the selected group can do search queries. If no group is selected, no LDAP Searches are allowed.",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal label=${msg("Bind mode")} name="bindMode">
|
|
<ak-radio
|
|
.options=${[
|
|
{
|
|
label: msg("Cached binding"),
|
|
value: LDAPAPIAccessMode.Cached,
|
|
default: true,
|
|
description: html`${msg(
|
|
"Flow is executed and session is cached in memory. Flow is executed when session expires",
|
|
)}`,
|
|
},
|
|
{
|
|
label: msg("Direct binding"),
|
|
value: LDAPAPIAccessMode.Direct,
|
|
description: html`${msg(
|
|
"Always execute the configured bind flow to authenticate the user",
|
|
)}`,
|
|
},
|
|
]}
|
|
.value=${this.instance?.bindMode}
|
|
>
|
|
</ak-radio>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg("Configure how the outpost authenticates requests.")}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal label=${msg("Search mode")} name="searchMode">
|
|
<ak-radio
|
|
.options=${[
|
|
{
|
|
label: msg("Cached querying"),
|
|
value: LDAPAPIAccessMode.Cached,
|
|
default: true,
|
|
description: html`${msg(
|
|
"The outpost holds all users and groups in-memory and will refresh every 5 Minutes",
|
|
)}`,
|
|
},
|
|
{
|
|
label: msg("Direct querying"),
|
|
value: LDAPAPIAccessMode.Direct,
|
|
description: html`${msg(
|
|
"Always returns the latest data, but slower than cached querying",
|
|
)}`,
|
|
},
|
|
]}
|
|
.value=${this.instance?.searchMode}
|
|
>
|
|
</ak-radio>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg("Configure how the outpost queries the core authentik server's users.")}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal name="mfaSupport">
|
|
<label class="pf-c-switch">
|
|
<input
|
|
class="pf-c-switch__input"
|
|
type="checkbox"
|
|
?checked=${first(this.instance?.mfaSupport, true)}
|
|
/>
|
|
<span class="pf-c-switch__toggle">
|
|
<span class="pf-c-switch__toggle-icon">
|
|
<i class="fas fa-check" aria-hidden="true"></i>
|
|
</span>
|
|
</span>
|
|
<span class="pf-c-switch__label">${msg("Code-based MFA Support")}</span>
|
|
</label>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon.",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
|
|
<ak-form-group .expanded=${true}>
|
|
<span slot="header"> ${msg("Protocol settings")} </span>
|
|
<div slot="body" class="pf-c-form">
|
|
<ak-form-element-horizontal
|
|
label=${msg("Base DN")}
|
|
?required=${true}
|
|
name="baseDn"
|
|
>
|
|
<input
|
|
type="text"
|
|
value="${first(this.instance?.baseDn, "DC=ldap,DC=goauthentik,DC=io")}"
|
|
class="pf-c-form-control"
|
|
required
|
|
/>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"LDAP DN under which bind requests and search requests can be made.",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
|
<ak-crypto-certificate-search
|
|
.certificate=${this.instance?.certificate}
|
|
></ak-crypto-certificate-search>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"The certificate for the above configured Base DN. As a fallback, the provider uses a self-signed certificate.",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal
|
|
label=${msg("TLS Server name")}
|
|
name="tlsServerName"
|
|
>
|
|
<input
|
|
type="text"
|
|
value="${first(this.instance?.tlsServerName, "")}"
|
|
class="pf-c-form-control"
|
|
required
|
|
/>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"DNS name for which the above configured certificate should be used. The certificate cannot be detected based on the base DN, as the SSL/TLS negotiation happens before such data is exchanged.",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal
|
|
label=${msg("UID start number")}
|
|
?required=${true}
|
|
name="uidStartNumber"
|
|
>
|
|
<input
|
|
type="number"
|
|
value="${first(this.instance?.uidStartNumber, 2000)}"
|
|
class="pf-c-form-control"
|
|
required
|
|
/>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"The start for uidNumbers, this number is added to the user.Pk to make sure that the numbers aren't too low for POSIX users. Default is 2000 to ensure that we don't collide with local users uidNumber",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
<ak-form-element-horizontal
|
|
label=${msg("GID start number")}
|
|
?required=${true}
|
|
name="gidStartNumber"
|
|
>
|
|
<input
|
|
type="number"
|
|
value="${first(this.instance?.gidStartNumber, 4000)}"
|
|
class="pf-c-form-control"
|
|
required
|
|
/>
|
|
<p class="pf-c-form__helper-text">
|
|
${msg(
|
|
"The start for gidNumbers, this number is added to a number generated from the group.Pk to make sure that the numbers aren't too low for POSIX groups. Default is 4000 to ensure that we don't collide with local groups or users primary groups gidNumber",
|
|
)}
|
|
</p>
|
|
</ak-form-element-horizontal>
|
|
</div>
|
|
</ak-form-group>`;
|
|
}
|
|
}
|