web/admin: add UI for reputations
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
31ad09c391
commit
c89b8a5f7c
|
@ -73,6 +73,12 @@ export class AdminInterface extends Interface {
|
||||||
<ak-sidebar-item path="/policy/policies">
|
<ak-sidebar-item path="/policy/policies">
|
||||||
<span slot="label">${t`Policies`}</span>
|
<span slot="label">${t`Policies`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
|
<ak-sidebar-item path="/policy/reputation/ip">
|
||||||
|
<span slot="label">${t`Reputation policy - IPs`}</span>
|
||||||
|
</ak-sidebar-item>
|
||||||
|
<ak-sidebar-item path="/policy/reputation/user">
|
||||||
|
<span slot="label">${t`Reputation policy - Users`}</span>
|
||||||
|
</ak-sidebar-item>
|
||||||
<ak-sidebar-item path="/core/property-mappings">
|
<ak-sidebar-item path="/core/property-mappings">
|
||||||
<span slot="label">${t`Property Mappings`}</span>
|
<span slot="label">${t`Property Mappings`}</span>
|
||||||
</ak-sidebar-item>
|
</ak-sidebar-item>
|
||||||
|
|
|
@ -985,6 +985,8 @@ msgstr "Define how notifications are sent to users, like Email or Webhook."
|
||||||
#: src/pages/outposts/OutpostListPage.ts
|
#: src/pages/outposts/OutpostListPage.ts
|
||||||
#: src/pages/outposts/ServiceConnectionListPage.ts
|
#: src/pages/outposts/ServiceConnectionListPage.ts
|
||||||
#: src/pages/policies/PolicyListPage.ts
|
#: src/pages/policies/PolicyListPage.ts
|
||||||
|
#: src/pages/policies/reputation/IPReputationListPage.ts
|
||||||
|
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||||
#: src/pages/property-mappings/PropertyMappingListPage.ts
|
#: src/pages/property-mappings/PropertyMappingListPage.ts
|
||||||
#: src/pages/providers/ProviderListPage.ts
|
#: src/pages/providers/ProviderListPage.ts
|
||||||
#: src/pages/sources/SourcesListPage.ts
|
#: src/pages/sources/SourcesListPage.ts
|
||||||
|
@ -1698,6 +1700,15 @@ msgstr "How many attempts a user has before the flow is canceled. To lock the us
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr "ID"
|
msgstr "ID"
|
||||||
|
|
||||||
|
#: src/pages/policies/reputation/IPReputationListPage.ts
|
||||||
|
msgid "IP"
|
||||||
|
msgstr "IP"
|
||||||
|
|
||||||
|
#: src/pages/policies/reputation/IPReputationListPage.ts
|
||||||
|
#: src/pages/policies/reputation/IPReputationListPage.ts
|
||||||
|
msgid "IP Reputation"
|
||||||
|
msgstr "IP Reputation"
|
||||||
|
|
||||||
#: src/pages/applications/ApplicationForm.ts
|
#: src/pages/applications/ApplicationForm.ts
|
||||||
msgid "Icon"
|
msgid "Icon"
|
||||||
msgstr "Icon"
|
msgstr "Icon"
|
||||||
|
@ -1796,6 +1807,10 @@ msgstr "Invalidation"
|
||||||
msgid "Invalidation flow"
|
msgid "Invalidation flow"
|
||||||
msgstr "Invalidation flow"
|
msgstr "Invalidation flow"
|
||||||
|
|
||||||
|
#: src/pages/stages/invitation/InvitationListPage.ts
|
||||||
|
msgid "Invitation"
|
||||||
|
msgstr "Invitation"
|
||||||
|
|
||||||
#: src/interfaces/AdminInterface.ts
|
#: src/interfaces/AdminInterface.ts
|
||||||
#: src/pages/stages/invitation/InvitationListPage.ts
|
#: src/pages/stages/invitation/InvitationListPage.ts
|
||||||
msgid "Invitations"
|
msgid "Invitations"
|
||||||
|
@ -2619,7 +2634,6 @@ msgstr "Private key, acquired from https://www.google.com/recaptcha/intro/v3.htm
|
||||||
msgid "Profile URL"
|
msgid "Profile URL"
|
||||||
msgstr "Profile URL"
|
msgstr "Profile URL"
|
||||||
|
|
||||||
#: src/pages/stages/invitation/InvitationListPage.ts
|
|
||||||
#: src/pages/stages/prompt/PromptListPage.ts
|
#: src/pages/stages/prompt/PromptListPage.ts
|
||||||
msgid "Prompt"
|
msgid "Prompt"
|
||||||
msgstr "Prompt"
|
msgstr "Prompt"
|
||||||
|
@ -2815,6 +2829,22 @@ msgstr "Reload"
|
||||||
msgid "Remove the user from the current session."
|
msgid "Remove the user from the current session."
|
||||||
msgstr "Remove the user from the current session."
|
msgstr "Remove the user from the current session."
|
||||||
|
|
||||||
|
#: src/pages/policies/reputation/IPReputationListPage.ts
|
||||||
|
msgid "Reputation for IPs. Scores are decreased for each failed login and increased for each successful login."
|
||||||
|
msgstr "Reputation for IPs. Scores are decreased for each failed login and increased for each successful login."
|
||||||
|
|
||||||
|
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||||
|
msgid "Reputation for usernames. Scores are decreased for each failed login and increased for each successful login."
|
||||||
|
msgstr "Reputation for usernames. Scores are decreased for each failed login and increased for each successful login."
|
||||||
|
|
||||||
|
#: src/interfaces/AdminInterface.ts
|
||||||
|
msgid "Reputation policy - IPs"
|
||||||
|
msgstr "Reputation policy - IPs"
|
||||||
|
|
||||||
|
#: src/interfaces/AdminInterface.ts
|
||||||
|
msgid "Reputation policy - Users"
|
||||||
|
msgstr "Reputation policy - Users"
|
||||||
|
|
||||||
#: src/pages/events/EventInfo.ts
|
#: src/pages/events/EventInfo.ts
|
||||||
#: src/pages/events/EventInfo.ts
|
#: src/pages/events/EventInfo.ts
|
||||||
msgid "Request"
|
msgid "Request"
|
||||||
|
@ -2942,6 +2972,11 @@ msgstr "Scope which the client can specify to access these properties."
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr "Scopes"
|
msgstr "Scopes"
|
||||||
|
|
||||||
|
#: src/pages/policies/reputation/IPReputationListPage.ts
|
||||||
|
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||||
|
msgid "Score"
|
||||||
|
msgstr "Score"
|
||||||
|
|
||||||
#: src/elements/table/TableSearch.ts
|
#: src/elements/table/TableSearch.ts
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr "Search..."
|
msgstr "Search..."
|
||||||
|
@ -4075,6 +4110,11 @@ msgstr "User Info"
|
||||||
msgid "User Property Mappings"
|
msgid "User Property Mappings"
|
||||||
msgstr "User Property Mappings"
|
msgstr "User Property Mappings"
|
||||||
|
|
||||||
|
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||||
|
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||||
|
msgid "User Reputation"
|
||||||
|
msgstr "User Reputation"
|
||||||
|
|
||||||
#: src/pages/user-settings/UserSettingsPage.ts
|
#: src/pages/user-settings/UserSettingsPage.ts
|
||||||
msgid "User Settings"
|
msgid "User Settings"
|
||||||
msgstr "User Settings"
|
msgstr "User Settings"
|
||||||
|
@ -4131,6 +4171,7 @@ msgid "Userinfo URL"
|
||||||
msgstr "Userinfo URL"
|
msgstr "Userinfo URL"
|
||||||
|
|
||||||
#: src/flows/stages/identification/IdentificationStage.ts
|
#: src/flows/stages/identification/IdentificationStage.ts
|
||||||
|
#: src/pages/policies/reputation/UserReputationListPage.ts
|
||||||
#: src/pages/stages/identification/IdentificationStageForm.ts
|
#: src/pages/stages/identification/IdentificationStageForm.ts
|
||||||
#: src/pages/user-settings/UserDetailsPage.ts
|
#: src/pages/user-settings/UserDetailsPage.ts
|
||||||
#: src/pages/users/UserForm.ts
|
#: src/pages/users/UserForm.ts
|
||||||
|
|
|
@ -990,6 +990,8 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
#:
|
||||||
|
#:
|
||||||
msgid "Delete"
|
msgid "Delete"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -1690,6 +1692,15 @@ msgstr ""
|
||||||
msgid "ID"
|
msgid "ID"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "IP"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
#:
|
||||||
|
msgid "IP Reputation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "Icon"
|
msgid "Icon"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -1788,6 +1799,10 @@ msgstr ""
|
||||||
msgid "Invalidation flow"
|
msgid "Invalidation flow"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Invitation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
msgid "Invitations"
|
msgid "Invitations"
|
||||||
|
@ -2611,7 +2626,6 @@ msgstr ""
|
||||||
msgid "Profile URL"
|
msgid "Profile URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#:
|
|
||||||
#:
|
#:
|
||||||
msgid "Prompt"
|
msgid "Prompt"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -2807,6 +2821,22 @@ msgstr ""
|
||||||
msgid "Remove the user from the current session."
|
msgid "Remove the user from the current session."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Reputation for IPs. Scores are decreased for each failed login and increased for each successful login."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Reputation for usernames. Scores are decreased for each failed login and increased for each successful login."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Reputation policy - IPs"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
msgid "Reputation policy - Users"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
msgid "Request"
|
msgid "Request"
|
||||||
|
@ -2934,6 +2964,11 @@ msgstr ""
|
||||||
msgid "Scopes"
|
msgid "Scopes"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
#:
|
||||||
|
msgid "Score"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "Search..."
|
msgid "Search..."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -4063,6 +4098,11 @@ msgstr ""
|
||||||
msgid "User Property Mappings"
|
msgid "User Property Mappings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#:
|
||||||
|
#:
|
||||||
|
msgid "User Reputation"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#:
|
#:
|
||||||
msgid "User Settings"
|
msgid "User Settings"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
@ -4123,6 +4163,7 @@ msgstr ""
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
#:
|
#:
|
||||||
|
#:
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
70
web/src/pages/policies/reputation/IPReputationListPage.ts
Normal file
70
web/src/pages/policies/reputation/IPReputationListPage.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||||
|
import { AKResponse } from "../../../api/Client";
|
||||||
|
import { TablePage } from "../../../elements/table/TablePage";
|
||||||
|
|
||||||
|
import "../../../elements/buttons/ModalButton";
|
||||||
|
import "../../../elements/buttons/SpinnerButton";
|
||||||
|
import "../../../elements/forms/DeleteForm";
|
||||||
|
import "../../../elements/forms/ModalForm";
|
||||||
|
import { TableColumn } from "../../../elements/table/Table";
|
||||||
|
import { PAGE_SIZE } from "../../../constants";
|
||||||
|
import { IPReputation, PoliciesApi } from "authentik-api";
|
||||||
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
|
|
||||||
|
@customElement("ak-policy-reputation-ip-list")
|
||||||
|
export class IPReputationListPage extends TablePage<IPReputation> {
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pageTitle(): string {
|
||||||
|
return t`IP Reputation`;
|
||||||
|
}
|
||||||
|
pageDescription(): string {
|
||||||
|
return t`Reputation for IPs. Scores are decreased for each failed login and increased for each successful login.`;
|
||||||
|
}
|
||||||
|
pageIcon(): string {
|
||||||
|
return "fa fa-ban";
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
order = "ip";
|
||||||
|
|
||||||
|
apiEndpoint(page: number): Promise<AKResponse<IPReputation>> {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationIpsList({
|
||||||
|
ordering: this.order,
|
||||||
|
page: page,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
search: this.search || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
columns(): TableColumn[] {
|
||||||
|
return [
|
||||||
|
new TableColumn(t`IP`, "ip"),
|
||||||
|
new TableColumn(t`Score`, "score"),
|
||||||
|
new TableColumn(""),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
row(item: IPReputation): TemplateResult[] {
|
||||||
|
return [
|
||||||
|
html`${item.ip}`,
|
||||||
|
html`${item.score}`,
|
||||||
|
html`
|
||||||
|
<ak-forms-delete
|
||||||
|
.obj=${item}
|
||||||
|
objectLabel=${t`IP Reputation`}
|
||||||
|
.delete=${() => {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationIpsDestroy({
|
||||||
|
id: item.pk,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||||
|
${t`Delete`}
|
||||||
|
</button>
|
||||||
|
</ak-forms-delete>`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
70
web/src/pages/policies/reputation/UserReputationListPage.ts
Normal file
70
web/src/pages/policies/reputation/UserReputationListPage.ts
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
import { t } from "@lingui/macro";
|
||||||
|
import { customElement, html, property, TemplateResult } from "lit-element";
|
||||||
|
import { AKResponse } from "../../../api/Client";
|
||||||
|
import { TablePage } from "../../../elements/table/TablePage";
|
||||||
|
|
||||||
|
import "../../../elements/buttons/ModalButton";
|
||||||
|
import "../../../elements/buttons/SpinnerButton";
|
||||||
|
import "../../../elements/forms/DeleteForm";
|
||||||
|
import "../../../elements/forms/ModalForm";
|
||||||
|
import { TableColumn } from "../../../elements/table/Table";
|
||||||
|
import { PAGE_SIZE } from "../../../constants";
|
||||||
|
import { UserReputation, PoliciesApi } from "authentik-api";
|
||||||
|
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||||
|
|
||||||
|
@customElement("ak-policy-reputation-user-list")
|
||||||
|
export class UserReputationListPage extends TablePage<UserReputation> {
|
||||||
|
searchEnabled(): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
pageTitle(): string {
|
||||||
|
return t`User Reputation`;
|
||||||
|
}
|
||||||
|
pageDescription(): string {
|
||||||
|
return t`Reputation for usernames. Scores are decreased for each failed login and increased for each successful login.`;
|
||||||
|
}
|
||||||
|
pageIcon(): string {
|
||||||
|
return "fa fa-ban";
|
||||||
|
}
|
||||||
|
|
||||||
|
@property()
|
||||||
|
order = "username";
|
||||||
|
|
||||||
|
apiEndpoint(page: number): Promise<AKResponse<UserReputation>> {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationUsersList({
|
||||||
|
ordering: this.order,
|
||||||
|
page: page,
|
||||||
|
pageSize: PAGE_SIZE,
|
||||||
|
search: this.search || "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
columns(): TableColumn[] {
|
||||||
|
return [
|
||||||
|
new TableColumn(t`Username`, "username"),
|
||||||
|
new TableColumn(t`Score`, "score"),
|
||||||
|
new TableColumn(""),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
row(item: UserReputation): TemplateResult[] {
|
||||||
|
return [
|
||||||
|
html`${item.username}`,
|
||||||
|
html`${item.score}`,
|
||||||
|
html`
|
||||||
|
<ak-forms-delete
|
||||||
|
.obj=${item}
|
||||||
|
objectLabel=${t`User Reputation`}
|
||||||
|
.delete=${() => {
|
||||||
|
return new PoliciesApi(DEFAULT_CONFIG).policiesReputationUsersDestroy({
|
||||||
|
id: item.pk,
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<button slot="trigger" class="pf-c-button pf-m-danger">
|
||||||
|
${t`Delete`}
|
||||||
|
</button>
|
||||||
|
</ak-forms-delete>`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -57,7 +57,7 @@ export class InvitationListPage extends TablePage<Invitation> {
|
||||||
html`
|
html`
|
||||||
<ak-forms-delete
|
<ak-forms-delete
|
||||||
.obj=${item}
|
.obj=${item}
|
||||||
objectLabel=${t`Prompt`}
|
objectLabel=${t`Invitation`}
|
||||||
.delete=${() => {
|
.delete=${() => {
|
||||||
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsDestroy({
|
return new StagesApi(DEFAULT_CONFIG).stagesInvitationInvitationsDestroy({
|
||||||
inviteUuid: item.pk || ""
|
inviteUuid: item.pk || ""
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { customElement } from "lit-element";
|
||||||
import { html, TemplateResult } from "lit-html";
|
import { html, TemplateResult } from "lit-html";
|
||||||
import { DEFAULT_CONFIG } from "../../api/Config";
|
import { DEFAULT_CONFIG } from "../../api/Config";
|
||||||
import "../../elements/forms/HorizontalFormElement";
|
import "../../elements/forms/HorizontalFormElement";
|
||||||
|
import "../../elements/forms/FormGroup";
|
||||||
import { first } from "../../utils";
|
import { first } from "../../utils";
|
||||||
import { ModelForm } from "../../elements/forms/ModelForm";
|
import { ModelForm } from "../../elements/forms/ModelForm";
|
||||||
import { until } from "lit-html/directives/until";
|
import { until } from "lit-html/directives/until";
|
||||||
|
|
|
@ -16,6 +16,8 @@ import "./pages/LibraryPage";
|
||||||
import "./pages/outposts/OutpostListPage";
|
import "./pages/outposts/OutpostListPage";
|
||||||
import "./pages/outposts/ServiceConnectionListPage";
|
import "./pages/outposts/ServiceConnectionListPage";
|
||||||
import "./pages/policies/PolicyListPage";
|
import "./pages/policies/PolicyListPage";
|
||||||
|
import "./pages/policies/reputation/IPReputationListPage";
|
||||||
|
import "./pages/policies/reputation/UserReputationListPage";
|
||||||
import "./pages/property-mappings/PropertyMappingListPage";
|
import "./pages/property-mappings/PropertyMappingListPage";
|
||||||
import "./pages/providers/ProviderListPage";
|
import "./pages/providers/ProviderListPage";
|
||||||
import "./pages/providers/ProviderViewPage";
|
import "./pages/providers/ProviderViewPage";
|
||||||
|
@ -54,6 +56,8 @@ export const ROUTES: Route[] = [
|
||||||
new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`),
|
new Route(new RegExp("^/core/tokens$"), html`<ak-token-list></ak-token-list>`),
|
||||||
new Route(new RegExp("^/core/tenants$"), html`<ak-tenant-list></ak-tenant-list>`),
|
new Route(new RegExp("^/core/tenants$"), html`<ak-tenant-list></ak-tenant-list>`),
|
||||||
new Route(new RegExp("^/policy/policies$"), html`<ak-policy-list></ak-policy-list>`),
|
new Route(new RegExp("^/policy/policies$"), html`<ak-policy-list></ak-policy-list>`),
|
||||||
|
new Route(new RegExp("^/policy/reputation/ip$"), html`<ak-policy-reputation-ip-list></ak-policy-reputation-ip-list>`),
|
||||||
|
new Route(new RegExp("^/policy/reputation/user$"), html`<ak-policy-reputation-user-list></ak-policy-reputation-user-list>`),
|
||||||
new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`),
|
new Route(new RegExp("^/identity/groups$"), html`<ak-group-list></ak-group-list>`),
|
||||||
new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`),
|
new Route(new RegExp("^/identity/users$"), html`<ak-user-list></ak-user-list>`),
|
||||||
new Route(new RegExp(`^/identity/users/(?<id>${ID_REGEX})$`)).then((args) => {
|
new Route(new RegExp(`^/identity/users/(?<id>${ID_REGEX})$`)).then((args) => {
|
||||||
|
|
|
@ -25,6 +25,7 @@ title: Next
|
||||||
## Minor changes
|
## Minor changes
|
||||||
|
|
||||||
- You can now specify which sources should be shown on an Identification stage.
|
- You can now specify which sources should be shown on an Identification stage.
|
||||||
|
- Add UI for the reputation of IPs and usernames for reputation policies.
|
||||||
|
|
||||||
## Upgrading
|
## Upgrading
|
||||||
|
|
||||||
|
|
Reference in a new issue