enterprise/providers: Add RAC [AUTH-15] (#7291)
* add basic guacamole Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make everything mostly work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add rac build to CI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix resize, fix web lint, sendSize correctly Signed-off-by: Jens Langhammer <jens@goauthentik.io> * pre-send connection from client, format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * improve throughput Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework TokenOutpostConsumer into middleware Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix some layout issues Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add outpost controllers Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start testing audio things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix a bunch of things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix to work with outpost group Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add simple loadbalancing Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add simple reconnect Signed-off-by: Jens Langhammer <jens@goauthentik.io> * show reconnecting text Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix error when checking ports Signed-off-by: Jens Langhammer <jens@goauthentik.io> * move to providers Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add flow check to interface Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix go lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix rac app label Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix audio Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add logging Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * allow overriding all settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix duplicate keyboard, debug high DPI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-add deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing __init__.py breaking model loading I love python Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * bump successful ws connection to info Signed-off-by: Jens Langhammer <jens@goauthentik.io> * hide cursor since guac draws that Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add clipboard support (bidirectional) Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make codespell not want to break the code Signed-off-by: Jens Langhammer <jens@goauthentik.io> * run pr comment in separate task Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start endpoint and property mapping stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more endpoint things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix event model_pk filtering with ints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: improve event display for changelog Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rebuild endpoint stuff again Signed-off-by: Jens Langhammer <jens@goauthentik.io> * idk special url Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more stuff, connect token with session Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add disconnect Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework disconnect cleanly disconnect from guacd instead of just letting the connection timeout Signed-off-by: Jens Langhammer <jens@goauthentik.io> * clear cache when creating outpost Signed-off-by: Jens Langhammer <jens@goauthentik.io> * support host:port and fix protocol Signed-off-by: Jens Langhammer <jens@goauthentik.io> * center smaller viewport Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework connection to wait more and stop after some time Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add policy control to endpoints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove provider protocol Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't switch to different outpost connection when already chosen Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start using property mappings, add static settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add some RAC mapping settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests for event changes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests and fix issues found by said tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add preview banner, move endpoints to main page Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add locale Signed-off-by: Jens Langhammer <jens@goauthentik.io> * auto-select endpoint if only one is available Signed-off-by: Jens Langhammer <jens@goauthentik.io> * backport https://github.com/goauthentik/authentik/pull/7831 to rac Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dont select property mappings on endpoints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make table modal only load when opened Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only auto-redirect when open Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix web deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * check for token expiry and terminate session Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-add endpoint name to title Signed-off-by: Jens Langhammer <jens@goauthentik.io> * disconnect connection when token is manually deleted Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add initial RAC docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add connection expiry setting to provider Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix flaky tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -21,6 +21,7 @@ import {
|
||||
OutpostsServiceConnectionsAllListRequest,
|
||||
PaginatedLDAPProviderList,
|
||||
PaginatedProxyProviderList,
|
||||
PaginatedRACProviderList,
|
||||
PaginatedRadiusProviderList,
|
||||
ProvidersApi,
|
||||
ServiceConnection,
|
||||
@ -38,7 +39,8 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
||||
providers?:
|
||||
| PaginatedProxyProviderList
|
||||
| PaginatedLDAPProviderList
|
||||
| PaginatedRadiusProviderList;
|
||||
| PaginatedRadiusProviderList
|
||||
| PaginatedRACProviderList;
|
||||
|
||||
defaultConfig?: OutpostDefaultConfig;
|
||||
|
||||
@ -73,6 +75,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
||||
applicationIsnull: false,
|
||||
});
|
||||
break;
|
||||
case OutpostTypeEnum.Rac:
|
||||
this.providers = await new ProvidersApi(DEFAULT_CONFIG).providersRacList({
|
||||
ordering: "name",
|
||||
applicationIsnull: false,
|
||||
});
|
||||
break;
|
||||
case OutpostTypeEnum.UnknownDefaultOpenApi:
|
||||
this.providers = undefined;
|
||||
}
|
||||
@ -133,6 +141,12 @@ export class OutpostForm extends ModelForm<Outpost, string> {
|
||||
>
|
||||
${msg("Radius")}
|
||||
</option>
|
||||
<option
|
||||
value=${OutpostTypeEnum.Rac}
|
||||
?selected=${this.instance?.type === OutpostTypeEnum.Rac}
|
||||
>
|
||||
${msg("RAC")}
|
||||
</option>
|
||||
</select>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Integration")} name="serviceConnection">
|
||||
|
@ -41,6 +41,8 @@ export function TypeToLabel(type?: OutpostTypeEnum): string {
|
||||
return msg("LDAP");
|
||||
case OutpostTypeEnum.Radius:
|
||||
return msg("Radius");
|
||||
case OutpostTypeEnum.Rac:
|
||||
return msg("RAC");
|
||||
case OutpostTypeEnum.UnknownDefaultOpenApi:
|
||||
return msg("Unknown type");
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSCIMForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
|
||||
|
195
web/src/admin/property-mappings/PropertyMappingRACForm.ts
Normal file
195
web/src/admin/property-mappings/PropertyMappingRACForm.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { first } from "@goauthentik/app/common/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { docLink } from "@goauthentik/common/global";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
|
||||
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 { PropertymappingsApi, RACPropertyMapping } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-property-mapping-rac-form")
|
||||
export class PropertyMappingLDAPForm extends ModelForm<RACPropertyMapping, string> {
|
||||
loadInstance(pk: string): Promise<RACPropertyMapping> {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsRacRetrieve({
|
||||
pmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
return msg("Successfully updated mapping.");
|
||||
} else {
|
||||
return msg("Successfully created mapping.");
|
||||
}
|
||||
}
|
||||
|
||||
async send(data: RACPropertyMapping): Promise<RACPropertyMapping> {
|
||||
if (this.instance) {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsRacUpdate({
|
||||
pmUuid: this.instance.pk || "",
|
||||
rACPropertyMappingRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new PropertymappingsApi(DEFAULT_CONFIG).propertymappingsRacCreate({
|
||||
rACPropertyMappingRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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-group .expanded=${true}>
|
||||
<span slot="header"> ${msg("General settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Username")}
|
||||
?required=${true}
|
||||
name="staticSettings.username"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.staticSettings.username)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Password")}
|
||||
?required=${true}
|
||||
name="staticSettings.password"
|
||||
>
|
||||
<input
|
||||
type="password"
|
||||
value="${ifDefined(this.instance?.staticSettings.password)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("RDP settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal name="staticSettings.ignore-cert">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(
|
||||
this.instance?.staticSettings["ignore-cert"],
|
||||
false,
|
||||
)}
|
||||
/>
|
||||
<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("Ignore server certificate")}</span
|
||||
>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="staticSettings.enable-wallpaper">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(
|
||||
this.instance?.staticSettings["enable-wallpaper"],
|
||||
false,
|
||||
)}
|
||||
/>
|
||||
<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("Enable wallpaper")}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="staticSettings.enable-font-smoothing">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(
|
||||
this.instance?.staticSettings["enable-font-smoothing"],
|
||||
false,
|
||||
)}
|
||||
/>
|
||||
<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("Enable font-smoothing")}</span>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="staticSettings.enable-full-window-drag">
|
||||
<label class="pf-c-switch">
|
||||
<input
|
||||
class="pf-c-switch__input"
|
||||
type="checkbox"
|
||||
?checked=${first(
|
||||
this.instance?.staticSettings["enable-full-window-drag"],
|
||||
false,
|
||||
)}
|
||||
/>
|
||||
<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("Enable full window dragging")}</span
|
||||
>
|
||||
</label>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Expression")}
|
||||
?required=${true}
|
||||
name="expression"
|
||||
>
|
||||
<ak-codemirror
|
||||
mode=${CodeMirrorMode.Python}
|
||||
value="${ifDefined(this.instance?.expression)}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Expression using Python.")}
|
||||
<a
|
||||
target="_blank"
|
||||
href="${docLink(
|
||||
"/docs/property-mappings/expression?utm_source=authentik",
|
||||
)}"
|
||||
>
|
||||
${msg("See documentation for a list of all variables.")}
|
||||
</a>
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingLDAPForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingNotification";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingRACForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingSAMLForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingScopeForm";
|
||||
import "@goauthentik/admin/property-mappings/PropertyMappingTestForm";
|
||||
|
@ -3,6 +3,7 @@ import "@goauthentik/admin/providers/ProviderWizard";
|
||||
import "@goauthentik/admin/providers/ldap/LDAPProviderForm";
|
||||
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderForm";
|
||||
import "@goauthentik/admin/providers/proxy/ProxyProviderForm";
|
||||
import "@goauthentik/admin/providers/rac/RACProviderForm";
|
||||
import "@goauthentik/admin/providers/radius/RadiusProviderForm";
|
||||
import "@goauthentik/admin/providers/saml/SAMLProviderForm";
|
||||
import "@goauthentik/admin/providers/scim/SCIMProviderForm";
|
||||
|
@ -1,6 +1,7 @@
|
||||
import "@goauthentik/admin/providers/ldap/LDAPProviderViewPage";
|
||||
import "@goauthentik/admin/providers/oauth2/OAuth2ProviderViewPage";
|
||||
import "@goauthentik/admin/providers/proxy/ProxyProviderViewPage";
|
||||
import "@goauthentik/admin/providers/rac/RACProviderViewPage";
|
||||
import "@goauthentik/admin/providers/radius/RadiusProviderViewPage";
|
||||
import "@goauthentik/admin/providers/saml/SAMLProviderViewPage";
|
||||
import "@goauthentik/admin/providers/scim/SCIMProviderViewPage";
|
||||
@ -65,6 +66,10 @@ export class ProviderViewPage extends AKElement {
|
||||
return html`<ak-provider-radius-view
|
||||
providerID=${ifDefined(this.provider.pk)}
|
||||
></ak-provider-radius-view>`;
|
||||
case "ak-provider-rac-form":
|
||||
return html`<ak-provider-rac-view
|
||||
providerID=${ifDefined(this.provider.pk)}
|
||||
></ak-provider-rac-view>`;
|
||||
default:
|
||||
return html`<p>Invalid provider type ${this.provider?.component}</p>`;
|
||||
}
|
||||
|
146
web/src/admin/providers/rac/EndpointForm.ts
Normal file
146
web/src/admin/providers/rac/EndpointForm.ts
Normal file
@ -0,0 +1,146 @@
|
||||
import { first } from "@goauthentik/app/common/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-radio-input";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
AuthModeEnum,
|
||||
Endpoint,
|
||||
PaginatedRACPropertyMappingList,
|
||||
PropertymappingsApi,
|
||||
ProtocolEnum,
|
||||
RacApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-rac-endpoint-form")
|
||||
export class EndpointForm extends ModelForm<Endpoint, string> {
|
||||
@property({ type: Number })
|
||||
providerID?: number;
|
||||
|
||||
propertyMappings?: PaginatedRACPropertyMappingList;
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsRacList({
|
||||
ordering: "name",
|
||||
});
|
||||
}
|
||||
|
||||
loadInstance(pk: string): Promise<Endpoint> {
|
||||
return new RacApi(DEFAULT_CONFIG).racEndpointsRetrieve({
|
||||
pbmUuid: pk,
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
return this.instance
|
||||
? msg("Successfully updated endpoint.")
|
||||
: msg("Successfully created endpoint.");
|
||||
}
|
||||
|
||||
async send(data: Endpoint): Promise<Endpoint> {
|
||||
data.authMode = AuthModeEnum.Prompt;
|
||||
if (!this.instance) {
|
||||
data.provider = this.providerID || 0;
|
||||
} else {
|
||||
data.provider = this.instance.provider;
|
||||
}
|
||||
if (this.instance) {
|
||||
return new RacApi(DEFAULT_CONFIG).racEndpointsPartialUpdate({
|
||||
pbmUuid: this.instance.pk || "",
|
||||
patchedEndpointRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new RacApi(DEFAULT_CONFIG).racEndpointsCreate({
|
||||
endpointRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`
|
||||
<ak-form-element-horizontal label=${msg("Name")} name="name" ?required=${true}>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.name)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Protocol")} ?required=${true} name="protocol">
|
||||
<ak-radio
|
||||
.options=${[
|
||||
{
|
||||
label: msg("RDP"),
|
||||
value: ProtocolEnum.Rdp,
|
||||
},
|
||||
{
|
||||
label: msg("SSH"),
|
||||
value: ProtocolEnum.Ssh,
|
||||
},
|
||||
{
|
||||
label: msg("VNC"),
|
||||
value: ProtocolEnum.Vnc,
|
||||
},
|
||||
]}
|
||||
.value=${this.instance?.protocol}
|
||||
>
|
||||
</ak-radio>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Host")} name="host" ?required=${true}>
|
||||
<input
|
||||
type="text"
|
||||
value="${ifDefined(this.instance?.host)}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">${msg("Hostname/IP to connect to.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
?required=${true}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.propertyMappings?.results.map((mapping) => {
|
||||
let selected = false;
|
||||
if (this.instance?.propertyMappings) {
|
||||
selected = Array.from(this.instance?.propertyMappings).some((su) => {
|
||||
return su == mapping.pk;
|
||||
});
|
||||
}
|
||||
return html`<option value=${ifDefined(mapping.pk)} ?selected=${selected}>
|
||||
${mapping.name}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-group>
|
||||
<span slot="header"> ${msg("Advanced settings")} </span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal label=${msg("Settings")} name="settings">
|
||||
<ak-codemirror
|
||||
mode="yaml"
|
||||
value="${YAML.stringify(first(this.instance?.settings, {}))}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">${msg("Connection settings.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
142
web/src/admin/providers/rac/EndpointList.ts
Normal file
142
web/src/admin/providers/rac/EndpointList.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import "@goauthentik/admin/policies/BoundPoliciesList";
|
||||
import "@goauthentik/app/admin/providers/rac/EndpointForm";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
import "@goauthentik/elements/rbac/ObjectPermissionModal";
|
||||
import { PaginatedResponse, Table } from "@goauthentik/elements/table/Table";
|
||||
import { TableColumn } from "@goauthentik/elements/table/Table";
|
||||
import "@patternfly/elements/pf-tooltip/pf-tooltip.js";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import {
|
||||
Endpoint,
|
||||
RACProvider,
|
||||
RacApi,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-rac-endpoint-list")
|
||||
export class EndpointListPage extends Table<Endpoint> {
|
||||
expandable = true;
|
||||
checkbox = true;
|
||||
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@property()
|
||||
order = "name";
|
||||
|
||||
@property({ attribute: false })
|
||||
provider?: RACProvider;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(PFDescriptionList);
|
||||
}
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Endpoint>> {
|
||||
return new RacApi(DEFAULT_CONFIG).racEndpointsList({
|
||||
ordering: this.order,
|
||||
page: page,
|
||||
pageSize: (await uiConfig()).pagination.perPage,
|
||||
search: this.search || "",
|
||||
provider: this.provider?.pk,
|
||||
superuserFullList: true,
|
||||
});
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [
|
||||
new TableColumn(msg("Name"), "name"),
|
||||
new TableColumn(msg("Host"), "host"),
|
||||
new TableColumn(msg("Actions")),
|
||||
];
|
||||
}
|
||||
|
||||
renderToolbarSelected(): TemplateResult {
|
||||
const disabled = this.selectedElements.length < 1;
|
||||
return html`<ak-forms-delete-bulk
|
||||
objectLabel=${msg("Endpoint(s)")}
|
||||
.objects=${this.selectedElements}
|
||||
.metadata=${(item: Endpoint) => {
|
||||
return [
|
||||
{ key: msg("Name"), value: item.name },
|
||||
{ key: msg("Host"), value: item.host },
|
||||
];
|
||||
}}
|
||||
.usedBy=${(item: Endpoint) => {
|
||||
return new RacApi(DEFAULT_CONFIG).racEndpointsUsedByList({
|
||||
pbmUuid: item.pk,
|
||||
});
|
||||
}}
|
||||
.delete=${(item: Endpoint) => {
|
||||
return new RacApi(DEFAULT_CONFIG).racEndpointsDestroy({
|
||||
pbmUuid: item.pk,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<button ?disabled=${disabled} slot="trigger" class="pf-c-button pf-m-danger">
|
||||
${msg("Delete")}
|
||||
</button>
|
||||
</ak-forms-delete-bulk>`;
|
||||
}
|
||||
|
||||
row(item: Endpoint): TemplateResult[] {
|
||||
return [
|
||||
html`${item.name}`,
|
||||
html`${item.host}`,
|
||||
html`<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update Endpoint")} </span>
|
||||
<ak-rac-endpoint-form slot="form" .instancePk=${item.pk}>
|
||||
</ak-rac-endpoint-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-plain">
|
||||
<pf-tooltip position="top" content=${msg("Edit")}>
|
||||
<i class="fas fa-edit"></i>
|
||||
</pf-tooltip>
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
<ak-rbac-object-permission-modal
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersRacEndpoint}
|
||||
objectPk=${item.pk}
|
||||
>
|
||||
</ak-rbac-object-permission-modal>`,
|
||||
];
|
||||
}
|
||||
|
||||
renderExpanded(item: Endpoint): TemplateResult {
|
||||
return html` <td></td>
|
||||
<td role="cell" colspan="4">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-c-content">
|
||||
<p>
|
||||
${msg(
|
||||
"These bindings control which users will have access to this endpoint. Users must also have access to the application.",
|
||||
)}
|
||||
</p>
|
||||
<ak-bound-policies-list .target=${item.pk}> </ak-bound-policies-list>
|
||||
</div>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
renderObjectCreate(): TemplateResult {
|
||||
return html`
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Create")} </span>
|
||||
<span slot="header"> ${msg("Create Endpoint")} </span>
|
||||
<ak-rac-endpoint-form slot="form" .providerID=${this.provider?.pk}>
|
||||
</ak-rac-endpoint-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">${msg("Create")}</button>
|
||||
</ak-forms-modal>
|
||||
`;
|
||||
}
|
||||
}
|
158
web/src/admin/providers/rac/RACProviderForm.ts
Normal file
158
web/src/admin/providers/rac/RACProviderForm.ts
Normal file
@ -0,0 +1,158 @@
|
||||
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||
import "@goauthentik/admin/common/ak-flow-search/ak-tenanted-flow-search";
|
||||
import { first } from "@goauthentik/app/common/utils";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/forms/FormGroup";
|
||||
import "@goauthentik/elements/forms/HorizontalFormElement";
|
||||
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
|
||||
import "@goauthentik/elements/forms/Radio";
|
||||
import "@goauthentik/elements/forms/SearchSelect";
|
||||
import "@goauthentik/elements/utils/TimeDeltaHelp";
|
||||
import YAML from "yaml";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators.js";
|
||||
import { ifDefined } from "lit/directives/if-defined.js";
|
||||
|
||||
import {
|
||||
FlowsInstancesListDesignationEnum,
|
||||
PaginatedEndpointList,
|
||||
PaginatedRACPropertyMappingList,
|
||||
PropertymappingsApi,
|
||||
ProvidersApi,
|
||||
RACProvider,
|
||||
RacApi,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-rac-form")
|
||||
export class RACProviderFormPage extends ModelForm<RACProvider, number> {
|
||||
@state()
|
||||
endpoints?: PaginatedEndpointList;
|
||||
|
||||
propertyMappings?: PaginatedRACPropertyMappingList;
|
||||
|
||||
async load(): Promise<void> {
|
||||
this.endpoints = await new RacApi(DEFAULT_CONFIG).racEndpointsList({});
|
||||
this.propertyMappings = await new PropertymappingsApi(
|
||||
DEFAULT_CONFIG,
|
||||
).propertymappingsRacList({
|
||||
ordering: "name",
|
||||
});
|
||||
}
|
||||
|
||||
async loadInstance(pk: number): Promise<RACProvider> {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersRacRetrieve({
|
||||
id: pk,
|
||||
});
|
||||
}
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.instance) {
|
||||
return msg("Successfully updated provider.");
|
||||
} else {
|
||||
return msg("Successfully created provider.");
|
||||
}
|
||||
}
|
||||
|
||||
async send(data: RACProvider): Promise<RACProvider> {
|
||||
if (this.instance) {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersRacUpdate({
|
||||
id: this.instance.pk || 0,
|
||||
rACProviderRequest: data,
|
||||
});
|
||||
} else {
|
||||
return new ProvidersApi(DEFAULT_CONFIG).providersRacCreate({
|
||||
rACProviderRequest: data,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
name="authorizationFlow"
|
||||
label=${msg("Authorization flow")}
|
||||
?required=${true}
|
||||
>
|
||||
<ak-flow-search
|
||||
flowType=${FlowsInstancesListDesignationEnum.Authorization}
|
||||
.currentFlow=${this.instance?.authorizationFlow}
|
||||
required
|
||||
></ak-flow-search>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Flow used when authorizing this provider.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Connection expiry")}
|
||||
?required=${true}
|
||||
name="connectionExpiry"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value="${first(this.instance?.connectionExpiry, "hours=8")}"
|
||||
class="pf-c-form-control"
|
||||
required
|
||||
/>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg(
|
||||
"Determines how long a session lasts before being disconnected and requiring re-authorization.",
|
||||
)}
|
||||
</p>
|
||||
<ak-utils-time-delta-help></ak-utils-time-delta-help>
|
||||
</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("Property mappings")}
|
||||
?required=${true}
|
||||
name="propertyMappings"
|
||||
>
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.propertyMappings?.results.map((mapping) => {
|
||||
let selected = false;
|
||||
if (this.instance?.propertyMappings) {
|
||||
selected = Array.from(this.instance?.propertyMappings).some(
|
||||
(su) => {
|
||||
return su == mapping.pk;
|
||||
},
|
||||
);
|
||||
}
|
||||
return html`<option
|
||||
value=${ifDefined(mapping.pk)}
|
||||
?selected=${selected}
|
||||
>
|
||||
${mapping.name}
|
||||
</option>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">
|
||||
${msg("Hold control/command to select multiple items.")}
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal label=${msg("Settings")} name="settings">
|
||||
<ak-codemirror
|
||||
mode="yaml"
|
||||
value="${YAML.stringify(first(this.instance?.settings, {}))}"
|
||||
>
|
||||
</ak-codemirror>
|
||||
<p class="pf-c-form__helper-text">${msg("Connection settings.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
`;
|
||||
}
|
||||
}
|
181
web/src/admin/providers/rac/RACProviderViewPage.ts
Normal file
181
web/src/admin/providers/rac/RACProviderViewPage.ts
Normal file
@ -0,0 +1,181 @@
|
||||
import "@goauthentik/admin/providers/RelatedApplicationButton";
|
||||
import "@goauthentik/admin/providers/rac/EndpointForm";
|
||||
import "@goauthentik/admin/providers/rac/EndpointList";
|
||||
import "@goauthentik/admin/providers/rac/RACProviderForm";
|
||||
import "@goauthentik/app/elements/rbac/ObjectPermissionsPage";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EVENT_REFRESH } from "@goauthentik/common/constants";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import "@goauthentik/components/events/ObjectChangelog";
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/CodeMirror";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/ModalButton";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import PFBanner from "@patternfly/patternfly/components/Banner/banner.css";
|
||||
import PFButton from "@patternfly/patternfly/components/Button/button.css";
|
||||
import PFCard from "@patternfly/patternfly/components/Card/card.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
import PFForm from "@patternfly/patternfly/components/Form/form.css";
|
||||
import PFFormControl from "@patternfly/patternfly/components/FormControl/form-control.css";
|
||||
import PFList from "@patternfly/patternfly/components/List/list.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFGrid from "@patternfly/patternfly/layouts/Grid/grid.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import {
|
||||
ProvidersApi,
|
||||
RACProvider,
|
||||
RbacPermissionsAssignedByUsersListModelEnum,
|
||||
} from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-provider-rac-view")
|
||||
export class RACProviderViewPage extends AKElement {
|
||||
@property()
|
||||
set args(value: { [key: string]: number }) {
|
||||
this.providerID = value.id;
|
||||
}
|
||||
|
||||
@property({ type: Number })
|
||||
set providerID(value: number) {
|
||||
new ProvidersApi(DEFAULT_CONFIG)
|
||||
.providersRacRetrieve({
|
||||
id: value,
|
||||
})
|
||||
.then((prov) => (this.provider = prov));
|
||||
}
|
||||
|
||||
@property({ attribute: false })
|
||||
provider?: RACProvider;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFButton,
|
||||
PFPage,
|
||||
PFGrid,
|
||||
PFContent,
|
||||
PFList,
|
||||
PFForm,
|
||||
PFFormControl,
|
||||
PFCard,
|
||||
PFDescriptionList,
|
||||
PFBanner,
|
||||
];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.addEventListener(EVENT_REFRESH, () => {
|
||||
if (!this.provider?.pk) return;
|
||||
this.providerID = this.provider?.pk;
|
||||
});
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
}
|
||||
return html`<ak-tabs>
|
||||
<section slot="page-overview" data-tab-title="${msg("Overview")}">
|
||||
${this.renderTabOverview()}
|
||||
</section>
|
||||
<section
|
||||
slot="page-changelog"
|
||||
data-tab-title="${msg("Changelog")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-c-card">
|
||||
<div class="pf-c-card__body">
|
||||
<ak-object-changelog
|
||||
targetModelPk=${this.provider?.pk || ""}
|
||||
targetModelName=${this.provider?.metaModelName || ""}
|
||||
>
|
||||
</ak-object-changelog>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ak-rbac-object-permission-page
|
||||
slot="page-permissions"
|
||||
data-tab-title="${msg("Permissions")}"
|
||||
model=${RbacPermissionsAssignedByUsersListModelEnum.ProvidersRacRacprovider}
|
||||
objectPk=${this.provider.pk}
|
||||
></ak-rbac-object-permission-page>
|
||||
</ak-tabs>`;
|
||||
}
|
||||
|
||||
renderTabOverview(): TemplateResult {
|
||||
if (!this.provider) {
|
||||
return html``;
|
||||
}
|
||||
return html` <div slot="header" class="pf-c-banner pf-m-info">
|
||||
${msg("RAC is in preview.")}
|
||||
<a href="mailto:hello+feature/rac@goauthentik.io">${msg("Send us feedback!")}</a>
|
||||
</div>
|
||||
${this.provider?.assignedApplicationName
|
||||
? html``
|
||||
: html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
${msg("Warning: Provider is not used by an Application.")}
|
||||
</div>`}
|
||||
${this.provider?.outpostSet.length < 1
|
||||
? html`<div slot="header" class="pf-c-banner pf-m-warning">
|
||||
${msg("Warning: Provider is not used by any Outpost.")}
|
||||
</div>`
|
||||
: html``}
|
||||
<div class="pf-c-page__main-section pf-m-no-padding-mobile pf-l-grid pf-m-gutter">
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__body">
|
||||
<dl class="pf-c-description-list pf-m-3-col-on-lg">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Name")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
${this.provider.name}
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text"
|
||||
>${msg("Assigned to application")}</span
|
||||
>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-provider-related-application
|
||||
.provider=${this.provider}
|
||||
></ak-provider-related-application>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="pf-c-card__footer">
|
||||
<ak-forms-modal>
|
||||
<span slot="submit"> ${msg("Update")} </span>
|
||||
<span slot="header"> ${msg("Update RAC Provider")} </span>
|
||||
<ak-provider-rac-form slot="form" .instancePk=${this.provider.pk || 0}>
|
||||
</ak-provider-rac-form>
|
||||
<button slot="trigger" class="pf-c-button pf-m-primary">
|
||||
${msg("Edit")}
|
||||
</button>
|
||||
</ak-forms-modal>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pf-c-card pf-l-grid__item pf-m-12-col">
|
||||
<div class="pf-c-card__title">${msg("Endpoints")}</div>
|
||||
<div class="pf-c-card__body">
|
||||
<ak-rac-endpoint-list .provider=${this.provider}> </ak-rac-endpoint-list>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
import { EventGeo } from "@goauthentik/app/admin/events/utils";
|
||||
import { actionToLabel } from "@goauthentik/app/common/labels";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { EventWithContext } from "@goauthentik/common/events";
|
||||
import { uiConfig } from "@goauthentik/common/ui/config";
|
||||
@ -73,7 +75,7 @@ export class ObjectChangelog extends Table<Event> {
|
||||
|
||||
row(item: EventWithContext): TemplateResult[] {
|
||||
return [
|
||||
html`${item.action}`,
|
||||
html`${actionToLabel(item.action)}`,
|
||||
html`<div>${item.user?.username}</div>
|
||||
${item.user.on_behalf_of
|
||||
? html`<small>
|
||||
@ -81,7 +83,9 @@ export class ObjectChangelog extends Table<Event> {
|
||||
</small>`
|
||||
: html``}`,
|
||||
html`<span>${item.created?.toLocaleString()}</span>`,
|
||||
html`<span>${item.clientIp || msg("-")}</span>`,
|
||||
html`<div>${item.clientIp || msg("-")}</div>
|
||||
|
||||
<small>${EventGeo(item)}</small>`,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { AKElement } from "@goauthentik/elements/Base";
|
||||
import { PFSize } from "@goauthentik/elements/Spinner";
|
||||
import "@goauthentik/elements/EmptyState";
|
||||
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
@ -33,6 +33,8 @@ export class LoadingOverlay extends AKElement {
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`<ak-spinner size=${PFSize.XLarge}></ak-spinner>`;
|
||||
return html`<ak-empty-state ?loading="${true}">
|
||||
<slot name="body" slot="body"></slot>
|
||||
</ak-empty-state>`;
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,11 @@ import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
import { Pagination, ResponseError } from "@goauthentik/api";
|
||||
|
||||
export interface TableLike {
|
||||
order?: string;
|
||||
fetch: () => void;
|
||||
}
|
||||
|
||||
export class TableColumn {
|
||||
title: string;
|
||||
orderBy?: string;
|
||||
@ -38,7 +43,7 @@ export class TableColumn {
|
||||
this.orderBy = orderBy;
|
||||
}
|
||||
|
||||
headerClickHandler(table: Table<unknown>): void {
|
||||
headerClickHandler(table: TableLike): void {
|
||||
if (!this.orderBy) {
|
||||
return;
|
||||
}
|
||||
@ -46,7 +51,7 @@ export class TableColumn {
|
||||
table.fetch();
|
||||
}
|
||||
|
||||
private getSortIndicator(table: Table<unknown>): string {
|
||||
private getSortIndicator(table: TableLike): string {
|
||||
switch (table.order) {
|
||||
case this.orderBy:
|
||||
return "fa-long-arrow-alt-down";
|
||||
@ -57,7 +62,7 @@ export class TableColumn {
|
||||
}
|
||||
}
|
||||
|
||||
renderSortable(table: Table<unknown>): TemplateResult {
|
||||
renderSortable(table: TableLike): TemplateResult {
|
||||
return html` <button
|
||||
class="pf-c-table__button"
|
||||
@click=${() => this.headerClickHandler(table)}
|
||||
@ -71,7 +76,7 @@ export class TableColumn {
|
||||
</button>`;
|
||||
}
|
||||
|
||||
render(table: Table<unknown>): TemplateResult {
|
||||
render(table: TableLike): TemplateResult {
|
||||
const classes = {
|
||||
"pf-c-table__sort": !!this.orderBy,
|
||||
"pf-m-selected": table.order === this.orderBy || table.order === `-${this.orderBy}`,
|
||||
@ -89,7 +94,7 @@ export interface PaginatedResponse<T> {
|
||||
results: Array<T>;
|
||||
}
|
||||
|
||||
export abstract class Table<T> extends AKElement {
|
||||
export abstract class Table<T> extends AKElement implements TableLike {
|
||||
abstract apiEndpoint(page: number): Promise<PaginatedResponse<T>>;
|
||||
abstract columns(): TableColumn[];
|
||||
abstract row(item: T): TemplateResult[];
|
||||
@ -123,6 +128,12 @@ export abstract class Table<T> extends AKElement {
|
||||
@property({ type: Boolean })
|
||||
checkbox = false;
|
||||
|
||||
@property({ type: Boolean })
|
||||
clickable = false;
|
||||
|
||||
@property({ attribute: false })
|
||||
clickHandler: (item: T) => void = () => {};
|
||||
|
||||
@property({ type: Boolean })
|
||||
radioSelect = false;
|
||||
|
||||
@ -356,8 +367,12 @@ export abstract class Table<T> extends AKElement {
|
||||
return html`<tbody role="rowgroup" class="${classMap(expandedClass)}">
|
||||
<tr
|
||||
role="row"
|
||||
class="${this.checkbox ? "pf-m-hoverable" : ""}"
|
||||
@click=${itemSelectHandler}
|
||||
class="${this.checkbox || this.clickable ? "pf-m-hoverable" : ""}"
|
||||
@click=${this.clickable
|
||||
? () => {
|
||||
this.clickHandler(item);
|
||||
}
|
||||
: itemSelectHandler}
|
||||
>
|
||||
${this.checkbox ? renderCheckbox() : html``}
|
||||
${this.expandable ? renderExpansion() : html``}
|
||||
|
@ -19,7 +19,18 @@ export abstract class TableModal<T> extends Table<T> {
|
||||
size: PFSize = PFSize.Large;
|
||||
|
||||
@property({ type: Boolean })
|
||||
open = false;
|
||||
set open(value: boolean) {
|
||||
this._open = value;
|
||||
if (value) {
|
||||
this.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
get open(): boolean {
|
||||
return this._open;
|
||||
}
|
||||
|
||||
_open = false;
|
||||
|
||||
static get styles(): CSSResult[] {
|
||||
return super.styles.concat(
|
||||
@ -43,6 +54,13 @@ export abstract class TableModal<T> extends Table<T> {
|
||||
});
|
||||
}
|
||||
|
||||
public async fetch(): Promise<void> {
|
||||
if (!this.open) {
|
||||
return;
|
||||
}
|
||||
return super.fetch();
|
||||
}
|
||||
|
||||
resetForms(): void {
|
||||
this.querySelectorAll<HTMLFormElement>("[slot=form]").forEach((form) => {
|
||||
if ("resetForm" in form) {
|
||||
|
324
web/src/enterprise/rac/index.ts
Normal file
324
web/src/enterprise/rac/index.ts
Normal file
@ -0,0 +1,324 @@
|
||||
import { TITLE_DEFAULT } from "@goauthentik/app/common/constants";
|
||||
import { Interface } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/LoadingOverlay";
|
||||
import Guacamole from "guacamole-common-js";
|
||||
|
||||
import { msg, str } from "@lit/localize";
|
||||
import { CSSResult, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators.js";
|
||||
|
||||
import AKGlobal from "@goauthentik/common/styles/authentik.css";
|
||||
import PFContent from "@patternfly/patternfly/components/Content/content.css";
|
||||
import PFPage from "@patternfly/patternfly/components/Page/page.css";
|
||||
import PFBase from "@patternfly/patternfly/patternfly-base.css";
|
||||
|
||||
enum GuacClientState {
|
||||
IDLE = 0,
|
||||
CONNECTING = 1,
|
||||
WAITING = 2,
|
||||
CONNECTED = 3,
|
||||
DISCONNECTING = 4,
|
||||
DISCONNECTED = 5,
|
||||
}
|
||||
|
||||
const AUDIO_INPUT_MIMETYPE = "audio/L16;rate=44100,channels=2";
|
||||
const RECONNECT_ATTEMPTS_INITIAL = 5;
|
||||
const RECONNECT_ATTEMPTS = 5;
|
||||
|
||||
@customElement("ak-rac")
|
||||
export class RacInterface extends Interface {
|
||||
static get styles(): CSSResult[] {
|
||||
return [
|
||||
PFBase,
|
||||
PFPage,
|
||||
PFContent,
|
||||
AKGlobal,
|
||||
css`
|
||||
:host {
|
||||
cursor: none;
|
||||
}
|
||||
canvas {
|
||||
z-index: unset !important;
|
||||
}
|
||||
.container {
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
background-color: black;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
ak-loading-overlay {
|
||||
z-index: 5;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
client?: Guacamole.Client;
|
||||
tunnel?: Guacamole.Tunnel;
|
||||
|
||||
@state()
|
||||
container?: HTMLElement;
|
||||
|
||||
@state()
|
||||
clientState?: GuacClientState;
|
||||
|
||||
@state()
|
||||
reconnectingMessage = "";
|
||||
|
||||
@property()
|
||||
token?: string;
|
||||
|
||||
@property()
|
||||
endpointName?: string;
|
||||
|
||||
@state()
|
||||
clipboardWatcherTimer = 0;
|
||||
|
||||
_previousClipboardValue: unknown;
|
||||
|
||||
// Set to `true` if we've successfully connected once
|
||||
hasConnected = false;
|
||||
// Keep track of current connection attempt
|
||||
connectionAttempt = 0;
|
||||
|
||||
static domSize(): DOMRect {
|
||||
return document.body.getBoundingClientRect();
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.initKeyboard();
|
||||
this.checkClipboard();
|
||||
this.clipboardWatcherTimer = setInterval(
|
||||
this.checkClipboard.bind(this),
|
||||
500,
|
||||
) as unknown as number;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
window.addEventListener(
|
||||
"focus",
|
||||
() => {
|
||||
this.checkClipboard();
|
||||
},
|
||||
{
|
||||
capture: false,
|
||||
},
|
||||
);
|
||||
window.addEventListener("resize", () => {
|
||||
this.client?.sendSize(
|
||||
Math.floor(RacInterface.domSize().width),
|
||||
Math.floor(RacInterface.domSize().height),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
clearInterval(this.clipboardWatcherTimer);
|
||||
}
|
||||
|
||||
async firstUpdated(): Promise<void> {
|
||||
this.updateTitle();
|
||||
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
|
||||
window.location.host
|
||||
}/ws/rac/${this.token}/`;
|
||||
this.tunnel = new Guacamole.WebSocketTunnel(wsUrl);
|
||||
this.tunnel.receiveTimeout = 10 * 1000; // 10 seconds
|
||||
this.tunnel.onerror = (status) => {
|
||||
console.debug("authentik/rac: tunnel error: ", status);
|
||||
this.reconnect();
|
||||
};
|
||||
this.client = new Guacamole.Client(this.tunnel);
|
||||
this.client.onerror = (err) => {
|
||||
console.debug("authentik/rac: error: ", err);
|
||||
this.reconnect();
|
||||
};
|
||||
this.client.onstatechange = (state) => {
|
||||
this.clientState = state;
|
||||
if (state === GuacClientState.CONNECTED) {
|
||||
this.onConnected();
|
||||
}
|
||||
};
|
||||
this.client.onclipboard = (stream, mimetype) => {
|
||||
// If the received data is text, read it as a simple string
|
||||
if (/^text\//.exec(mimetype)) {
|
||||
const reader = new Guacamole.StringReader(stream);
|
||||
let data = "";
|
||||
reader.ontext = (text) => {
|
||||
data += text;
|
||||
};
|
||||
reader.onend = () => {
|
||||
this._previousClipboardValue = data;
|
||||
navigator.clipboard.writeText(data);
|
||||
};
|
||||
} else {
|
||||
const reader = new Guacamole.BlobReader(stream, mimetype);
|
||||
reader.onend = () => {
|
||||
const blob = reader.getBlob();
|
||||
navigator.clipboard.write([
|
||||
new ClipboardItem({
|
||||
[blob.type]: blob,
|
||||
}),
|
||||
]);
|
||||
};
|
||||
}
|
||||
console.debug("authentik/rac: updated clipboard from remote");
|
||||
};
|
||||
const params = new URLSearchParams();
|
||||
params.set("screen_width", Math.floor(RacInterface.domSize().width).toString());
|
||||
params.set("screen_height", Math.floor(RacInterface.domSize().height).toString());
|
||||
params.set("screen_dpi", (window.devicePixelRatio * 96).toString());
|
||||
this.client.connect(params.toString());
|
||||
}
|
||||
|
||||
reconnect(): void {
|
||||
this.clientState = undefined;
|
||||
this.connectionAttempt += 1;
|
||||
if (!this.hasConnected) {
|
||||
// Check connection attempts if we haven't had a successful connection
|
||||
if (this.connectionAttempt >= RECONNECT_ATTEMPTS_INITIAL) {
|
||||
this.hasConnected = true;
|
||||
this.reconnectingMessage = msg(
|
||||
str`Connection failed after ${this.connectionAttempt} attempts.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (this.connectionAttempt >= RECONNECT_ATTEMPTS) {
|
||||
this.reconnectingMessage = msg(
|
||||
str`Connection failed after ${this.connectionAttempt} attempts.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const delay = 500 * this.connectionAttempt;
|
||||
this.reconnectingMessage = msg(
|
||||
str`Re-connecting in ${Math.max(1, delay / 1000)} second(s).`,
|
||||
);
|
||||
setTimeout(() => {
|
||||
this.firstUpdated();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
updateTitle(): void {
|
||||
let title = this.tenant?.brandingTitle || TITLE_DEFAULT;
|
||||
if (this.endpointName) {
|
||||
title = `${this.endpointName} - ${title}`;
|
||||
}
|
||||
document.title = `${title}`;
|
||||
}
|
||||
|
||||
onConnected(): void {
|
||||
console.debug("authentik/rac: connected");
|
||||
if (!this.client) {
|
||||
return;
|
||||
}
|
||||
this.hasConnected = true;
|
||||
this.container = this.client.getDisplay().getElement();
|
||||
this.initMouse(this.container);
|
||||
this.client?.sendSize(
|
||||
Math.floor(RacInterface.domSize().width),
|
||||
Math.floor(RacInterface.domSize().height),
|
||||
);
|
||||
}
|
||||
|
||||
initMouse(container: HTMLElement): void {
|
||||
const mouse = new Guacamole.Mouse(container);
|
||||
const handler = (mouseState: Guacamole.Mouse.State, scaleMouse = false) => {
|
||||
if (!this.client) return;
|
||||
|
||||
if (scaleMouse) {
|
||||
mouseState.y = mouseState.y / this.client.getDisplay().getScale();
|
||||
mouseState.x = mouseState.x / this.client.getDisplay().getScale();
|
||||
}
|
||||
|
||||
this.client.sendMouseState(mouseState);
|
||||
};
|
||||
mouse.onmouseup = mouse.onmousedown = (mouseState) => {
|
||||
this.container?.focus();
|
||||
handler(mouseState);
|
||||
};
|
||||
mouse.onmousemove = (mouseState) => {
|
||||
handler(mouseState, true);
|
||||
};
|
||||
}
|
||||
|
||||
initAudioInput(): void {
|
||||
const stream = this.client?.createAudioStream(AUDIO_INPUT_MIMETYPE);
|
||||
if (!stream) return;
|
||||
// Guacamole.AudioPlayer
|
||||
const recorder = Guacamole.AudioRecorder.getInstance(stream, AUDIO_INPUT_MIMETYPE);
|
||||
// If creation of the AudioRecorder failed, simply end the stream
|
||||
if (!recorder) {
|
||||
stream.sendEnd();
|
||||
return;
|
||||
}
|
||||
// Otherwise, ensure that another audio stream is created after this
|
||||
// audio stream is closed
|
||||
recorder.onclose = this.initAudioInput.bind(this);
|
||||
}
|
||||
|
||||
initKeyboard(): void {
|
||||
const keyboard = new Guacamole.Keyboard(document);
|
||||
keyboard.onkeydown = (keysym) => {
|
||||
this.client?.sendKeyEvent(1, keysym);
|
||||
};
|
||||
keyboard.onkeyup = (keysym) => {
|
||||
this.client?.sendKeyEvent(0, keysym);
|
||||
};
|
||||
}
|
||||
|
||||
async checkClipboard(): Promise<void> {
|
||||
try {
|
||||
if (!this._previousClipboardValue) {
|
||||
this._previousClipboardValue = await navigator.clipboard.readText();
|
||||
return;
|
||||
}
|
||||
const newValue = await navigator.clipboard.readText();
|
||||
if (newValue !== this._previousClipboardValue) {
|
||||
console.debug(`authentik/rac: new clipboard value: ${newValue}`);
|
||||
this._previousClipboardValue = newValue;
|
||||
this.writeClipboard(newValue);
|
||||
}
|
||||
} catch (ex) {
|
||||
// The error is most likely caused by the document not being in focus
|
||||
// in which case we can ignore it and just retry
|
||||
if (ex instanceof DOMException) {
|
||||
return;
|
||||
}
|
||||
console.warn("authentik/rac: error reading clipboard", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private writeClipboard(value: string) {
|
||||
if (!this.client) {
|
||||
return;
|
||||
}
|
||||
const stream = this.client.createClipboardStream("text/plain", "clipboard");
|
||||
const writer = new Guacamole.StringWriter(stream);
|
||||
writer.sendText(value);
|
||||
writer.sendEnd();
|
||||
console.debug("authentik/rac: Sent clipboard");
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
return html`
|
||||
${this.clientState !== GuacClientState.CONNECTED
|
||||
? html`
|
||||
<ak-loading-overlay>
|
||||
<span slot="body">
|
||||
${this.hasConnected
|
||||
? html`${this.reconnectingMessage}`
|
||||
: html`${msg("Connecting...")}`}
|
||||
</span>
|
||||
</ak-loading-overlay>
|
||||
`
|
||||
: html``}
|
||||
<div class="container">${this.container}</div>
|
||||
`;
|
||||
}
|
||||
}
|
71
web/src/user/LibraryApplication/RACLaunchEndpointModal.ts
Normal file
71
web/src/user/LibraryApplication/RACLaunchEndpointModal.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/app/common/api/config";
|
||||
import { PaginatedResponse, TableColumn } from "@goauthentik/app/elements/table/Table";
|
||||
import { TableModal } from "@goauthentik/app/elements/table/TableModal";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
import { TemplateResult, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import { Application, Endpoint, RacApi } from "@goauthentik/api";
|
||||
|
||||
@customElement("ak-library-rac-endpoint-launch")
|
||||
export class RACLaunchEndpointModal extends TableModal<Endpoint> {
|
||||
clickable = true;
|
||||
searchEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
clickHandler = (item: Endpoint) => {
|
||||
if (!item.launchUrl) {
|
||||
return;
|
||||
}
|
||||
if (this.app?.openInNewTab) {
|
||||
window.open(item.launchUrl);
|
||||
} else {
|
||||
window.location.assign(item.launchUrl);
|
||||
}
|
||||
};
|
||||
|
||||
@property({ attribute: false })
|
||||
app?: Application;
|
||||
|
||||
async apiEndpoint(page: number): Promise<PaginatedResponse<Endpoint>> {
|
||||
const endpoints = await new RacApi(DEFAULT_CONFIG).racEndpointsList({
|
||||
provider: this.app?.provider || 0,
|
||||
page: page,
|
||||
search: this.search,
|
||||
});
|
||||
if (this.open && endpoints.pagination.count === 1) {
|
||||
this.clickHandler(endpoints.results[0]);
|
||||
this.open = false;
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
columns(): TableColumn[] {
|
||||
return [new TableColumn("Name")];
|
||||
}
|
||||
|
||||
row(item: Endpoint): TemplateResult[] {
|
||||
return [html`${item.name}`];
|
||||
}
|
||||
|
||||
renderModalInner(): TemplateResult {
|
||||
return html`<section class="pf-c-modal-box__header pf-c-page__main-section pf-m-light">
|
||||
<div class="pf-c-content">
|
||||
<h1 class="pf-c-title pf-m-2xl">${msg("Select endpoint to connect to")}</h1>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pf-c-modal-box__body pf-m-light">${this.renderTable()}</section>
|
||||
<footer class="pf-c-modal-box__footer">
|
||||
<ak-spinner-button
|
||||
.callAction=${async () => {
|
||||
this.open = false;
|
||||
}}
|
||||
class="pf-m-secondary"
|
||||
>
|
||||
${msg("Cancel")}
|
||||
</ak-spinner-button>
|
||||
</footer>`;
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ import { truncateWords } from "@goauthentik/common/utils";
|
||||
import "@goauthentik/components/ak-app-icon";
|
||||
import { AKElement, rootInterface } from "@goauthentik/elements/Base";
|
||||
import "@goauthentik/elements/Expand";
|
||||
import "@goauthentik/user/LibraryApplication/RACLaunchEndpointModal";
|
||||
import { UserInterface } from "@goauthentik/user/UserInterface";
|
||||
|
||||
import { msg } from "@lit/localize";
|
||||
@ -85,6 +86,22 @@ export class LibraryApplication extends AKElement {
|
||||
</ak-expand>`;
|
||||
}
|
||||
|
||||
renderLaunch(): TemplateResult {
|
||||
if (!this.application) {
|
||||
return html``;
|
||||
}
|
||||
if (this.application?.launchUrl === "goauthentik.io://providers/rac/launch") {
|
||||
return html`<ak-library-rac-endpoint-launch .app=${this.application}>
|
||||
<a slot="trigger"> ${this.application.name} </a>
|
||||
</ak-library-rac-endpoint-launch>`;
|
||||
}
|
||||
return html`<a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
|
||||
>${this.application.name}</a
|
||||
>`;
|
||||
}
|
||||
|
||||
render(): TemplateResult {
|
||||
if (!this.application) {
|
||||
return html`<ak-spinner></ak-spinner>`;
|
||||
@ -111,13 +128,7 @@ export class LibraryApplication extends AKElement {
|
||||
<ak-app-icon size=${PFSize.Large} .app=${this.application}></ak-app-icon>
|
||||
</a>
|
||||
</div>
|
||||
<div class="pf-c-card__title">
|
||||
<a
|
||||
href="${ifDefined(this.application.launchUrl ?? "")}"
|
||||
target="${ifDefined(this.application.openInNewTab ? "_blank" : undefined)}"
|
||||
>${this.application.name}</a
|
||||
>
|
||||
</div>
|
||||
<div class="pf-c-card__title">${this.renderLaunch()}</div>
|
||||
<div class="expander"></div>
|
||||
${expandable ? this.renderExpansion(this.application) : nothing}
|
||||
</div>`;
|
||||
|
@ -2,10 +2,16 @@ import type { Application } from "@goauthentik/api";
|
||||
|
||||
const isFullUrlRe = new RegExp("://");
|
||||
const isHttpRe = new RegExp("http(s?)://");
|
||||
const isAuthentikSpecialRe = new RegExp("goauthentik.io://");
|
||||
const isNotFullUrl = (url: string) => !isFullUrlRe.test(url);
|
||||
const isHttp = (url: string) => isHttpRe.test(url);
|
||||
const isAuthentikSpecial = (url: string) => isAuthentikSpecialRe.test(url);
|
||||
|
||||
export const appHasLaunchUrl = (app: Application) => {
|
||||
const url = app.launchUrl;
|
||||
return !!(typeof url === "string" && url !== "" && (isHttp(url) || isNotFullUrl(url)));
|
||||
return !!(
|
||||
typeof url === "string" &&
|
||||
url !== "" &&
|
||||
(isHttp(url) || isNotFullUrl(url) || isAuthentikSpecial(url))
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user