diff --git a/webapp/apps/gateway-ui/package.json b/webapp/apps/gateway-ui/package.json index e307cdda4..37eab3527 100644 --- a/webapp/apps/gateway-ui/package.json +++ b/webapp/apps/gateway-ui/package.json @@ -30,13 +30,15 @@ "@devolutions/iron-remote-desktop": "^0.11.0", "@devolutions/iron-remote-desktop-rdp": "^0.7.0", "@devolutions/iron-remote-desktop-vnc": "0.7.0", + "@devolutions/ldap-wasm-js": "^0.3.4", "@devolutions/terminal-shared": "^1.4.1", + "@devolutions/web-active-directory-gui": "0.0.13", "@devolutions/web-ssh-gui": "^0.7.0", "@devolutions/web-telnet-gui": "^0.4.4", "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^1.2.5", - "@primeuix/themes": "^1.2.5", - "@primeuix/utils": "^0.6.3", + "@primeuix/themes": "2.0.3", + "@primeuix/utils": "0.7.2", "@xterm/addon-clipboard": "^0.1.0", "@xterm/xterm": "^5.5.0", "i18next": "^25.5.3", diff --git a/webapp/apps/gateway-ui/src/assets/css/main.scss b/webapp/apps/gateway-ui/src/assets/css/main.scss index 27eea1f40..494b2855a 100644 --- a/webapp/apps/gateway-ui/src/assets/css/main.scss +++ b/webapp/apps/gateway-ui/src/assets/css/main.scss @@ -9,6 +9,8 @@ @use 'dvls-stack/variables.generated' as *; @use 'dvls-stack/theme/light-theme' as *; @use 'dvls-stack/theme/dark-theme' as *; +@use '@devolutions/web-active-directory-gui/assets/css/ad-styles' as *; +@use '../../client/app/styles/ad-theme-bridge.gateway' as *; @use 'style/base' as *; @use 'style/entry-status' as *; @@ -57,6 +59,16 @@ line-height: 1; } +/* PrimeIcons must always win over generic PrimeNG font overrides. */ +.pi, +[class^="pi-"], +[class*=" pi-"] { + font-family: 'primeicons' !important; + font-style: normal; + font-weight: normal; + line-height: 1; +} + .full-width-dropdown .p-dropdown, .full-width-dropdown .p-select { width: 100%; diff --git a/webapp/apps/gateway-ui/src/client/app/app.constants.ts b/webapp/apps/gateway-ui/src/client/app/app.constants.ts index 7b0b65b05..8d5e3b954 100644 --- a/webapp/apps/gateway-ui/src/client/app/app.constants.ts +++ b/webapp/apps/gateway-ui/src/client/app/app.constants.ts @@ -5,6 +5,7 @@ export const DVL_TELNET_ICON = 'dvl-icon-entry-session-telnet'; export const DVL_SSH_ICON = 'dvl-icon-entry-session-ssh'; export const DVL_VNC_ICON = 'dvl-icon-entry-session-vnc'; export const DVL_ARD_ICON = 'dvl-icon-entry-session-apple-remote-desktop'; +export const DVL_ACTIVE_DIRECTORY_ICON = 'dvl-icon-active-directory'; export const DVL_WARNING_ICON = 'dvl-icon-warning'; export const JET_RDP_URL = '/jet/rdp'; @@ -12,6 +13,7 @@ export const JET_TELNET_URL = '/jet/fwd/tcp'; export const JET_SSH_URL = '/jet/fwd/tcp'; export const JET_VNC_URL = '/jet/fwd/tcp'; export const JET_ARD_URL = '/jet/fwd/tcp'; +export const JET_AD_URL = '/jet/fwd'; export const JET_KDC_PROXY_URL = '/jet/KdcProxy'; export const ProtocolIconMap = { @@ -20,6 +22,7 @@ export const ProtocolIconMap = { [Protocol.SSH]: DVL_SSH_ICON, [Protocol.VNC]: DVL_VNC_ICON, [Protocol.ARD]: DVL_ARD_ICON, + [Protocol.ActiveDirectory]: DVL_ACTIVE_DIRECTORY_ICON, }; export const ProtocolNameToProtocolMap = { @@ -28,4 +31,6 @@ export const ProtocolNameToProtocolMap = { telnet: Protocol.Telnet, rdp: Protocol.RDP, ard: Protocol.ARD, + ldap: Protocol.ActiveDirectory, + ldaps: Protocol.ActiveDirectory, }; diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts new file mode 100644 index 000000000..0c74bee9f --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-data.service.ts @@ -0,0 +1,440 @@ +import { Injectable, NgZone } from '@angular/core'; +import init, { + Attribute, + AttributesArray, + BinaryLdapModifies, + LdapControl, + LdapControlArray, + LdapResult, + LdapSession, + LdapSessionParameters, + LoggingLevel, + ModifyRequest, + SaslBindConfig, + SearchParameters, + SspiAuthMethod, + set_logging_level, +} from '@devolutions/ldap-wasm-js'; +import { + AdAddRequest, + AdBindRequest, + AdBindResult, + AdCapabilities, + AdDataProvider, + AdDeleteRequest, + AdModifyDnRequest, + AdModifyRequest, + AdPageControl, + AdPageResult, + AdResult, + AdSearchParams, + AdSearchResult, + LdapControlArrayLike, + LdapResultLike, + LdapSessionLike, + normalizeError, + SearchEntryLike, + SearchMessageLike, +} from '@devolutions/web-active-directory-gui'; +import { ActiveDirectorySessionStoreService } from './active-directory-session-store.service'; + +const LDAP_DECODER_MAX_BYTES = 32656; +const LDAP_PAGED_SEARCH_SIZE_LIMIT = 100000; + +@Injectable() +export class ActiveDirectoryDataService implements AdDataProvider { + private wasmInitialized = false; + private wasmInitPromise: Promise | null = null; + + constructor( + private readonly zone: NgZone, + private readonly sessionStore: ActiveDirectorySessionStoreService, + ) {} + + async search(params: AdSearchParams): Promise { + const session = this.sessionStore.getSession(); + + try { + const result = await this.zone.runOutsideAngular(() => + session.search({ + search_base: params.baseDn, + filter: params.filter, + scope: params.scope, + attributes: params.attributes || [], + size_limit: undefined, + time_limit: params.timeLimit, + controls: params.controls, + }), + ); + + const entries = this.extractSearchEntries(result.messages || []); + + return { + entries, + total: entries.length, + nextCookie: null, + control: undefined, + }; + } catch (error) { + throw normalizeError(error); + } + } + + async pagedSearch(ctrl: AdPageControl): Promise { + const session = this.sessionStore.getSession(); + + try { + const controlsWithPaging: LdapControlArrayLike = [ + ...(ctrl.controls || []), + { + simple_paged_results: { + size: ctrl.pageSize, + cookie: ctrl.cookie ?? [], + }, + }, + ]; + + const result = await this.zone.runOutsideAngular(() => + session.search({ + search_base: ctrl.baseDn, + filter: ctrl.filter, + scope: ctrl.scope, + attributes: ctrl.attributes || [], + size_limit: LDAP_PAGED_SEARCH_SIZE_LIMIT, + time_limit: ctrl.timeLimit, + controls: controlsWithPaging, + }), + ); + + const entries = this.extractSearchEntries(result.messages || []); + const searchDone = result.messages?.find((message) => 'search_done' in message.op); + const responseControl = searchDone?.ctrl ?? []; + const pagedControl = responseControl.find( + (ldapControl): ldapControl is { simple_paged_results: { size: number; cookie: number[] } } => + 'simple_paged_results' in ldapControl, + ); + const cookie = pagedControl?.simple_paged_results.cookie ?? []; + + return { + entries, + hasMore: cookie.length > 0, + cookie, + control: responseControl, + }; + } catch (error) { + throw normalizeError(error); + } + } + + async getCapabilities(): Promise { + return { + canCreateUser: true, + canCreateGroup: true, + canResetPassword: true, + canDelete: true, + }; + } + + async connect(gatewayWsUrl: string): Promise { + try { + await this.initializeWasm(); + + const ldapSession = await this.zone.runOutsideAngular(() => + LdapSession.connect(new LdapSessionParameters(gatewayWsUrl, LDAP_DECODER_MAX_BYTES)), + ); + + const wrappedSession: LdapSessionLike = { + sasl_bind: async (args) => { + const response = await this.zone.runOutsideAngular(() => ldapSession.sasl_bind(this.toSaslBindConfig(args))); + + return { + res: response.res, + serverSaslCredential: response.saslcreds, + }; + }, + unbind: (ldapControl) => + this.zone.runOutsideAngular(() => ldapSession.unbind(this.toLdapControls(ldapControl))), + search: async (args) => { + const result = await this.zone.runOutsideAngular(() => ldapSession.search(this.toSearchParameters(args))); + + return { + messages: result.messages.flatMap((message): SearchMessageLike[] => { + if ('search_entry' in message.op) { + return [{ op: { search_entry: message.op.search_entry }, ctrl: message.ctrl }]; + } + + if ('search_done' in message.op) { + return [{ op: { search_done: message.op.search_done }, ctrl: message.ctrl }]; + } + + return []; + }), + }; + }, + modify: (dn, modifies, controls) => + this.zone.runOutsideAngular(() => + ldapSession.modify(dn, this.toBinaryLdapModifies(modifies), this.toLdapControls(controls)), + ), + modifyDn: (dn, newRdn, deleteOldRdn, newSuperior, controls) => + this.zone.runOutsideAngular(() => + ldapSession.modify_dn(dn, newRdn, deleteOldRdn, newSuperior, this.toLdapControls(controls)), + ), + delete: (dn, controls) => + this.zone.runOutsideAngular(() => ldapSession.delete(dn, this.toLdapControls(controls))), + add: (dn, attributes, controls) => + this.zone.runOutsideAngular(() => + ldapSession.add(dn, this.toAttributesArray(attributes), this.toLdapControls(controls)), + ), + }; + + this.sessionStore.setSession(wrappedSession); + + return wrappedSession; + } catch (error) { + throw normalizeError(error); + } + } + + async bind(req: AdBindRequest): Promise { + try { + const { domain, serverComputerName } = this.resolveDomainAndServerComputerName( + req.domain, + req.serverComputerName, + ); + const response = await req.session.sasl_bind({ + username: req.username, + password: req.password, + auth_method: { + negotiate: { + domain, + server_computer_name: serverComputerName, + kdc_proxy_url: req.kdcProxyUrl, + }, + }, + sign: req.sign, + seal: req.seal, + controls: undefined, + }); + + return response.res; + } catch (error) { + throw normalizeError(error); + } + } + + async unbind(session: LdapSessionLike, control?: LdapControlArrayLike): Promise { + try { + await session.unbind(control); + this.sessionStore.clearSession(); + } catch (error) { + throw normalizeError(error); + } + } + + makeGatewayWsUrl(gatewayUrl: string | URL, sessionId: string, isLdaps: boolean, token?: string): string { + const url = typeof gatewayUrl === 'string' ? new URL(gatewayUrl, window.location.href) : new URL(gatewayUrl.href); + const forwardProtocol = isLdaps ? 'tls' : 'tcp'; + + url.pathname = `${url.pathname.replace(/\/$/, '')}/${forwardProtocol}/${sessionId}`; + url.search = token ? `?token=${token}` : ''; + url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'; + + return url.href; + } + + async addEntry(req: AdAddRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => req.session.add(req.dn, req.attributes, req.controls)); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async deleteEntry(req: AdDeleteRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => req.session.delete(req.dn, req.controls)); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async modifyEntry(req: AdModifyRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => req.session.modify(req.dn, req.modifies, req.controls)); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async modifyDnEntry(req: AdModifyDnRequest): Promise { + try { + const result = await this.zone.runOutsideAngular(() => + req.session.modifyDn(req.dn, req.newRdn, req.deleteOldRdn, req.newSuperior ?? null, req.controls), + ); + return this.toAdResult(result); + } catch (error) { + throw normalizeError(error); + } + } + + async initializeWasm(): Promise { + if (this.wasmInitialized) { + return; + } + + if (!this.wasmInitPromise) { + this.wasmInitPromise = (async () => { + await this.zone.runOutsideAngular(() => init()); + set_logging_level(LoggingLevel.Warn); + this.wasmInitialized = true; + })(); + } + + return this.wasmInitPromise; + } + + private extractSearchEntries(messages: { op: { search_entry?: SearchEntryLike } }[]): SearchEntryLike[] { + return messages.flatMap((message) => (message.op.search_entry ? [message.op.search_entry] : [])); + } + + private toSearchParameters(args: { + search_base: string; + filter: string; + scope: SearchParameters['scope']; + attributes: string[]; + size_limit?: number; + time_limit?: number; + controls?: LdapControlArrayLike; + }): SearchParameters { + return { + search_base: args.search_base, + filter: args.filter, + scope: args.scope, + attributes: args.attributes, + size_limit: args.size_limit, + time_limit: args.time_limit, + controls: this.toLdapControls(args.controls), + }; + } + + private toLdapControls(controls?: LdapControlArrayLike | null): LdapControlArray | undefined { + return controls?.map((control) => this.toLdapControl(control)); + } + + private toLdapControl(control: LdapControlArrayLike[number]): LdapControl { + if (this.isSimplePagedResultsControl(control)) { + return { + simple_paged_results: { + size: control.simple_paged_results.size, + cookie: control.simple_paged_results.cookie, + }, + }; + } + + return control as LdapControl; + } + + private isSimplePagedResultsControl( + control: LdapControlArrayLike[number], + ): control is { simple_paged_results: { size: number; cookie: number[] } } { + return ( + 'simple_paged_results' in control && + typeof control.simple_paged_results === 'object' && + control.simple_paged_results !== null && + 'size' in control.simple_paged_results && + typeof control.simple_paged_results.size === 'number' && + 'cookie' in control.simple_paged_results && + Array.isArray(control.simple_paged_results.cookie) + ); + } + + private toSaslBindConfig(args: Parameters[0]): SaslBindConfig { + return { + username: args.username, + password: args.password, + auth_method: this.toSspiAuthMethod(args.auth_method), + sign: args.sign, + seal: args.seal, + controls: this.toLdapControls(args.controls), + }; + } + + private toBinaryLdapModifies(modifies: Parameters[1]): BinaryLdapModifies { + return modifies.map( + (modify): ModifyRequest => ({ + operation: modify.operation, + attribute: this.toLdapAttribute(modify.attribute), + }), + ); + } + + private toAttributesArray(attributes: Parameters[1]): AttributesArray { + return attributes.map((attribute) => this.toLdapAttribute(attribute)); + } + + private toLdapAttribute(attribute: Parameters[1][number]): Attribute { + return { + attribute_name: attribute.attribute_name, + attribute_value: attribute.attribute_value, + }; + } + + private toSspiAuthMethod(authMethod: unknown): SspiAuthMethod { + if (this.isSspiAuthMethod(authMethod)) { + return authMethod; + } + + throw new Error('invalid active directory authentication method'); + } + + private isSspiAuthMethod(authMethod: unknown): authMethod is SspiAuthMethod { + if (typeof authMethod !== 'object' || authMethod === null) { + return false; + } + + return 'negotiate' in authMethod || 'kerberos' in authMethod || 'ntlm' in authMethod; + } + + private toAdResult(result: LdapResultLike | LdapResult): AdResult { + return { + code: String(result.code), + message: result.message, + matchedDn: this.getMatchedDn(result), + referral: result.referral, + }; + } + + private getMatchedDn(result: LdapResultLike | LdapResult): string | undefined { + if ('matchedDn' in result && typeof result.matchedDn === 'string') { + return result.matchedDn; + } + + if ('matcheddn' in result && typeof result.matcheddn === 'string') { + return result.matcheddn; + } + + return undefined; + } + + private resolveDomainAndServerComputerName( + domain: string | undefined, + serverComputerName: string, + ): { domain: string | undefined; serverComputerName: string } { + if (domain) { + return { + domain, + serverComputerName: `${serverComputerName.replace(`.${domain}`, '')}.${domain}`, + }; + } + + const parts = serverComputerName.split('.'); + + return { + domain: parts.length > 1 ? parts.slice(1).join('.') : undefined, + serverComputerName, + }; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts new file mode 100644 index 000000000..2436138a6 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-manager.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from '@angular/core'; +import { AdSessionManager, AdWebSessionConfig } from '@devolutions/web-active-directory-gui'; +import { Observable, of, throwError } from 'rxjs'; + +@Injectable() +export class ActiveDirectorySessionManagerService implements AdSessionManager { + private readonly sessionConfigMap = new Map(); + + setWebSessionConfig(config: AdWebSessionConfig): void { + this.sessionConfigMap.set(config.sessionId, config); + } + + clearWebSessionConfig(sessionId: string): void { + this.sessionConfigMap.delete(sessionId); + } + + getWebSessionConfig(sessionId: string): Observable { + const config = this.sessionConfigMap.get(sessionId); + + if (!config) { + return throwError(() => new Error(`Active Directory session config not found for session ${sessionId}`)); + } + + return of(config); + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts new file mode 100644 index 000000000..7098b927a --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-session-store.service.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@angular/core'; +import { LdapSessionLike } from '@devolutions/web-active-directory-gui'; + +@Injectable() +export class ActiveDirectorySessionStoreService { + private session: LdapSessionLike | null = null; + + setSession(session: LdapSessionLike): void { + this.session = session; + } + + getSession(): LdapSessionLike { + if (!this.session) { + throw new Error('No active LDAP session'); + } + + return this.session; + } + + clearSession(): void { + this.session = null; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts new file mode 100644 index 000000000..f3bea5dd7 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-translator.service.ts @@ -0,0 +1,187 @@ +import { Injectable } from '@angular/core'; +import { AdTranslator } from '@devolutions/web-active-directory-gui'; + +@Injectable() +export class ActiveDirectoryTranslatorService implements AdTranslator { + private readonly translations: Record = { + btnCancel: 'Cancel', + btnClose: 'Close', + btnCreate: 'Create', + btnDelete: 'Delete', + btnEdit: 'Edit', + btnNo: 'No', + btnOk: 'OK', + btnSave: 'Save', + btnYes: 'Yes', + ActiveDirectoryGroupScope_DomainLocal: 'Domain local', + ActiveDirectoryGroupScope_Global: 'Global', + ActiveDirectoryGroupScope_Universal: 'Universal', + ActiveDirectoryGroupType_Distribution: 'Distribution', + ActiveDirectoryGroupType_Security: 'Security', + AlreadyExists: 'Already exists', + AnErrorOccurredWhileSearchingForX: 'An error occurred while searching for {0}', + AnUnexpectedErrorOccured: 'An unexpected error occurred', + BitLockerMessageFormat: + 'Recovery Password:\r\n {0}\r\n {1}\r\n\r\nComputer: {2}\r\nDate: {3}\r\nPassword ID: {4}', + ConnectionLogMessage_ResetPassword: 'Reset password', + ConnectionLogMessage_UserResetPassword: 'User reset password', + CopiedToClipboard: 'Copied to clipboard', + DoYouReallyWantToDeleteX: 'Do you really want to delete {0}?', + DoYouReallyWantToDisableTheSelectedUser: 'Do you really want to disable the selected user?', + DoYouReallyWantToEnableTheSelectedUser: 'Do you really want to enable the selected user?', + DoYouWantToUnlockTheUserX: 'Do you want to unlock the user {0}?', + FailedToGetChildGroupsOfActiveDirectoryGroupX: 'Failed to get child groups of Active Directory group {0}', + MissingRequiredSettings: 'Missing required settings', + OperationFailed: 'Operation failed', + TheGroupHasBeenSuccessfullyCreated: 'The group has been successfully created', + TheObjectHasBeenSuccessfullyDeleted: 'The object has been successfully deleted', + ThePasswordHasBeenSuccessfullyReset: 'The password has been successfully reset', + TheUserHasBeenSuccessfullyCreated: 'The user has been successfully created', + TheUserHasBeenSuccessfullyDisabled: 'The user has been successfully disabled.', + TheUserHasBeenSuccessfullyEnabled: 'The user has been successfully enabled', + TheUserHasBeenSuccessfullyUnlocked: 'The user has been successfully unlocked', + UnableToCopy: 'Unable to Copy', + lblNo: 'No', + lblYes: 'Yes', + YourConnectionIsNotReady: 'Your connection is not ready', + YouMustFillAllFieldsBeforeContinuing: 'You must fill in all fields before continuing.', + OperationNotPermitted: 'Operation not permitted', + OperationsError: 'Operations error', + error: 'Error', + errBindFailed: 'Authentication failed', + errConnectionFailed: 'Failed to connect to Active Directory', + errInvalidCredentials: 'Invalid credentials', + errOperationFailed: 'Operation failed', + errPermissionDenied: 'Permission denied', + errSearchFailed: 'Search failed', + errTimeout: 'Operation timed out', + ldapAlreadyExists: 'Object already exists', + ldapAuthMethodNotSupported: 'Authentication method not supported', + ldapBusy: 'Server is busy', + ldapConstraintViolation: 'Constraint violation', + ldapInsufficientAccessRights: 'Insufficient access rights', + ldapInvalidCredentials: 'Invalid credentials', + ldapInvalidDNSyntax: 'Invalid DN syntax', + ldapNoSuchObject: 'Object not found', + ldapOperationsError: 'Operations error', + ldapProtocolError: 'Protocol error', + ldapSizeLimitExceeded: 'Size limit exceeded', + ldapStrongAuthRequired: 'Strong authentication required', + ldapSuccess: 'Operation completed successfully', + ldapTimeLimitExceeded: 'Time limit exceeded', + ldapUnavailable: 'Server is unavailable', + ldapUnwillingToPerform: 'Server unwilling to perform', + ldapdisplayname: 'Display Name', + lblAccountName: 'Account Name', + lblActiveDirectoryBitLockerFind: 'Find BitLocker Recovery Password', + lblActiveDirectoryPropertyListEditor: 'Property List Editor', + lblActiveDirectoryPropertyValueEditor: 'Property Value Editor', + lblActiveDirectoryUserProperties: 'User Properties', + lblAdd: 'Add', + lblAddMembers: 'Add Members', + lblAllPrinters: 'All Printers', + lblAttribute: 'Attribute', + lblAttributes: 'Attributes', + lblBitLocker: 'BitLocker', + lblBitLockerRecoveryPassword: 'BitLocker Recovery Password', + lblCancel: 'Cancel', + lblClear: 'Clear', + lblCloseSession: 'Close Session', + lblCommonName: 'Name', + lblComputer: 'Computer', + lblConfirm: 'Confirm', + lblConnect: 'Connect', + lblContainer: 'Container', + lblCopyToClipboard: 'Copy to Clipboard', + lblCreationDate: 'Creation Date', + lblDelete: 'Delete', + lblDisable: 'Disable', + lblDisabled: 'Disabled', + lblDisconnect: 'Disconnect', + lblDistinguishedName: 'Distinguished Name', + lblDomain: 'Domain', + lblEdit: 'Edit', + lblEnable: 'Enable', + lblEnterTextToSearch: 'Enter text to search', + lblExpand: 'Expand', + lblFilter: 'Filter', + lblFullName: 'Full Name', + lblGroup: 'Group', + lblGroupName: 'Group Name', + lblGroupPolicySettings: 'Group Policy Settings', + lblGroupProperties: 'Group Properties', + lblGroupScope: 'Group Scope', + lblGroupType: 'Group Type', + lblHidePassword: 'Hide Password', + lblLastModifiedOn: 'Last Modified', + lblLoading: 'Loading...', + lblMember: 'Member', + lblMemberOf: 'Member Of', + lblMembers: 'Members', + lblMore: 'More', + lblMustChangePasswordAtNextLogon: 'Must change password at next logon', + lblName: 'Name', + lblNewGroup: 'New Group', + lblNewUser: 'New User', + lblNoDataToDisplay: 'No data to display', + lblNoResultsFound: 'No results found', + lblOk: 'OK', + lblOrganizationalUnit: 'Organizational Unit', + lblPassword: 'Password', + lblPasswordID: 'Password ID', + lblProperties: 'Properties', + lblRecentlyDisabled: 'Recently Disabled', + lblRecentlyLocked: 'Recently Locked', + lblRecentlyModified: 'Recently Modified', + lblRefresh: 'Refresh', + lblRemove: 'Remove', + lblResults: 'Results', + lblRevealPassword: 'Reveal Password', + lblSamAccountName: 'SAM Account Name', + lblSave: 'Save', + lblSearch: 'Search', + lblSelect: 'Select', + lblType: 'Type', + lblUnlock: 'Unlock', + lblUPNLogonName: 'UPN Logon Name', + lblUser: 'User', + lblUserCreatedButPasswordFailed: 'User created, but password update failed', + lblValue: 'Value', + lblValues: 'Values', + lblValueToAdd: 'Value to Add', + lblWarning: 'Warning', + msgConfirmDelete: 'Are you sure you want to delete this item?', + msgConnected: 'Connected to Active Directory', + msgConnectionLost: 'Connection to Active Directory lost', + msgDeleteSuccess: 'Item deleted successfully', + msgDisconnected: 'Disconnected from Active Directory', + msgError: 'An error occurred', + msgNoResults: 'No results found', + msgSaveSuccess: 'Changes saved successfully', + msgSessionTerminated: 'Session has been terminated', + msgSuccess: 'Success', + }; + + translate(key: string, valuesToInclude?: Record | Array): string { + const translatedKey = this.translations[key] ?? key; + + if (!valuesToInclude) { + return translatedKey; + } + + if (Array.isArray(valuesToInclude)) { + return valuesToInclude.reduce( + (translated, value, index) => translated.replace(new RegExp(`\\{${index}\\}`, 'g'), String(value)), + translatedKey, + ); + } + + return Object.entries(valuesToInclude).reduce( + (translated, [name, value]) => + translated + .replace(new RegExp(`\\{${name}\\}`, 'g'), String(value)) + .replace(new RegExp(`{{\\s*${name}\\s*}}`, 'g'), String(value)), + translatedKey, + ); + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts new file mode 100644 index 000000000..32560bdfe --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/services/active-directory-ui.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { AdUi, AdUiConfirmOptions, AdUiToastOptions } from '@devolutions/web-active-directory-gui'; +import { ConfirmationService, MessageService } from 'primeng/api'; +import { ActiveDirectoryTranslatorService } from './active-directory-translator.service'; + +const AD_CONFIRM_KEY = 'adc-confirm-dialog'; +const AD_TOAST_KEY = 'adc-toast'; + +@Injectable() +export class ActiveDirectoryUiService implements AdUi { + private loadingCount = 0; + + constructor( + private readonly confirmationService: ConfirmationService, + private readonly messageService: MessageService, + private readonly translator: ActiveDirectoryTranslatorService, + ) {} + + toast(input: AdUiToastOptions): void { + this.messageService.add({ + key: input.key ?? AD_TOAST_KEY, + severity: input.severity, + summary: input.summary, + detail: input.detail, + life: input.life ?? 5000, + sticky: input.sticky, + }); + } + + clearToast(key?: string): void { + this.messageService.clear(key); + } + + confirm(input: AdUiConfirmOptions): void { + this.confirmationService.confirm({ + key: input.key ?? AD_CONFIRM_KEY, + header: this.translator.translate('lblConfirm'), + message: input.message, + acceptLabel: input.acceptLabel ?? this.translator.translate('lblYes'), + rejectLabel: input.rejectLabel ?? this.translator.translate('lblNo'), + acceptButtonStyleClass: 'p-button-sm', + rejectButtonStyleClass: 'p-button-sm p-button-text', + accept: input.accept, + reject: () => input.reject?.(), + }); + } + + showLoading(): void { + this.loadingCount++; + document.body.style.cursor = 'wait'; + } + + hideLoading(): void { + this.loadingCount = Math.max(0, this.loadingCount - 1); + + if (this.loadingCount === 0) { + document.body.style.cursor = 'default'; + } + } + + warn(message: string): void { + this.toast({ + severity: 'warn', + summary: this.translator.translate('lblWarning'), + detail: message, + }); + } + + error(message?: string, detail?: string): void { + this.toast({ + severity: 'error', + summary: message ?? this.translator.translate('error'), + detail, + sticky: true, + }); + } + + success(message?: string, detail?: string): void { + this.toast({ + severity: 'success', + summary: message ?? this.translator.translate('msgSuccess'), + detail, + }); + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html new file mode 100644 index 000000000..c64af004a --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.html @@ -0,0 +1,32 @@ +
+ @if (renderActiveDirectory) { + + } +
+ + + + diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss new file mode 100644 index 000000000..4f096256e --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.scss @@ -0,0 +1,101 @@ +:host { + display: flex; + height: 100%; + width: 100%; + flex-direction: column; + background-color: var(--connection-view-default-background-color, #fff) !important; + background-image: none !important; +} + +.active-directory-session-container { + display: flex; + flex-direction: column; + height: 100%; +} + +:host ::ng-deep .ad-dvl-toast.p-toast { + top: 68px; + right: 12px; + width: min(400px, calc(100vw - 24px)); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message { + margin: 0 0 8px; + border: 1px solid transparent; + border-radius: 6px; + box-shadow: none; + overflow: hidden; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-content { + min-height: 52px; + padding: 12px 14px; + display: flex; + align-items: center; + gap: 12px; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-success { + border-color: var(--success-message-border-color); + background: var(--success-message-background-color); + color: var(--success-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-info { + border-color: var(--info-message-border-color); + background: var(--info-message-background-color); + color: var(--info-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-warn { + border-color: var(--warning-message-border-color); + background: var(--warning-message-background-color); + color: var(--warning-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-error { + border-color: var(--error-message-border-color); + background: var(--error-message-background-color); + color: var(--error-message-border-color); +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-icon { + margin: 0; + width: 16px; + height: 16px; + font-size: 14px; + color: currentColor; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-message-text { + margin: 0; + flex: 1 1 auto; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-summary { + display: block; + color: currentColor; + font-size: 14px; + font-weight: 600; + line-height: 1.3; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-detail { + margin: 2px 0 0; + color: currentColor; + opacity: 1; + font-size: 14px; + font-weight: 600; + line-height: 20px; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-icon-close { + color: currentColor; + width: 28px; + height: 28px; + border-radius: 4px; +} + +:host ::ng-deep .ad-dvl-toast .p-toast-icon-close:hover { + background: rgba(var(--base-color-rgb), 0.06); +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts new file mode 100644 index 000000000..0e86523db --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/active-directory/web-client-active-directory.component.ts @@ -0,0 +1,398 @@ +import { + AfterViewInit, + Component, + ElementRef, + EventEmitter, + HostBinding, + OnDestroy, + Output, + ViewChild, +} from '@angular/core'; +import { + ActiveDirectoryConnectionParams, + ActiveDirectoryMainHandle, + AD_DATA_PROVIDER, + AD_SESSION_MANAGER, + AD_TRANSLATOR, + AD_UI, + AdConnectionError, + AdConnectionEventType, + matchTranslateKey, +} from '@devolutions/web-active-directory-gui'; +import { DVL_ACTIVE_DIRECTORY_ICON, DVL_WARNING_ICON, JET_AD_URL } from '@gateway/app.constants'; +import { WebClientBaseComponent } from '@shared/bases/base-web-client.component'; +import { GatewayAlertMessageService } from '@shared/components/gateway-alert-message/gateway-alert-message.service'; +import { ActiveDirectoryConnectionParameters } from '@shared/interfaces/connection-params.interfaces'; +import { ActiveDirectoryFormDataInput } from '@shared/interfaces/forms.interfaces'; +import { ComponentStatus } from '@shared/models/component-status.model'; +import { AnalyticService, ProtocolString } from '@shared/services/analytic.service'; +import { UtilsService } from '@shared/services/utils.service'; +import { DefaultLdapPort, DefaultLdapsPort, WebClientService } from '@shared/services/web-client.service'; +import { WebSessionService } from '@shared/services/web-session.service'; +import { EMPTY, from, Observable, of, Subscription } from 'rxjs'; +import { catchError, map, switchMap, takeUntil } from 'rxjs/operators'; +import { v4 as uuidv4 } from 'uuid'; +import { ActiveDirectoryDataService } from './services/active-directory-data.service'; +import { ActiveDirectorySessionManagerService } from './services/active-directory-session-manager.service'; +import { ActiveDirectorySessionStoreService } from './services/active-directory-session-store.service'; +import { ActiveDirectoryTranslatorService } from './services/active-directory-translator.service'; +import { ActiveDirectoryUiService } from './services/active-directory-ui.service'; + +@Component({ + standalone: false, + templateUrl: 'web-client-active-directory.component.html', + styleUrls: ['web-client-active-directory.component.scss'], + providers: [ + ActiveDirectoryDataService, + ActiveDirectorySessionManagerService, + ActiveDirectorySessionStoreService, + ActiveDirectoryTranslatorService, + ActiveDirectoryUiService, + { provide: AD_DATA_PROVIDER, useExisting: ActiveDirectoryDataService }, + { provide: AD_SESSION_MANAGER, useExisting: ActiveDirectorySessionManagerService }, + { provide: AD_TRANSLATOR, useExisting: ActiveDirectoryTranslatorService }, + { provide: AD_UI, useExisting: ActiveDirectoryUiService }, + ], +}) +export class WebClientActiveDirectoryComponent extends WebClientBaseComponent implements AfterViewInit, OnDestroy { + @HostBinding('attr.data-ad-theme-bridge') readonly adBridge = '1'; + + @Output() readonly componentStatus = new EventEmitter(); + @Output() readonly sizeChange = new EventEmitter(); + + @ViewChild('activeDirectoryContainer') activeDirectoryContainerRef: ElementRef; + + formData: ActiveDirectoryFormDataInput; + renderActiveDirectory = false; + + private activeDirectoryHandle: ActiveDirectoryMainHandle | null = null; + private activeDirectoryEventsSubscription: Subscription | null = null; + private renderActiveDirectoryTimeout: ReturnType | null = null; + + constructor( + protected gatewayAlertMessageService: GatewayAlertMessageService, + protected analyticService: AnalyticService, + private readonly utils: UtilsService, + private readonly webClientService: WebClientService, + private readonly webSessionService: WebSessionService, + private readonly activeDirectoryDataService: ActiveDirectoryDataService, + private readonly activeDirectorySessionManager: ActiveDirectorySessionManagerService, + private readonly activeDirectoryTranslator: ActiveDirectoryTranslatorService, + private readonly activeDirectoryUiService: ActiveDirectoryUiService, + ) { + super(gatewayAlertMessageService, analyticService); + } + + ngAfterViewInit(): void { + this.renderActiveDirectoryTimeout = setTimeout(() => { + this.renderActiveDirectory = true; + this.renderActiveDirectoryTimeout = null; + }); + } + + override ngOnDestroy(): void { + if (this.renderActiveDirectoryTimeout) { + clearTimeout(this.renderActiveDirectoryTimeout); + this.renderActiveDirectoryTimeout = null; + } + + this.activeDirectoryEventsSubscription?.unsubscribe(); + + if (this.currentStatus.isInitialized && !this.currentStatus.isDisabled) { + this.activeDirectoryHandle?.close(); + this.webClientConnectionClosed(); + } + + this.activeDirectorySessionManager.clearWebSessionConfig(this.webSessionId); + + super.ngOnDestroy(); + } + + onMainReady(handle: ActiveDirectoryMainHandle): void { + this.activeDirectoryHandle = handle; + this.subscribeToActiveDirectoryEvents(handle); + this.startConnectionProcess(); + } + + startTerminationProcess(): void { + this.currentStatus.isDisabledByUser = true; + this.sendTerminateSessionCmd(); + this.disableComponentStatus(); + this.webClientConnectionClosed(); + } + + sendTerminateSessionCmd(): void { + if (!this.activeDirectoryHandle || !this.currentStatus.isInitialized) { + return; + } + + this.currentStatus.isInitialized = false; + this.activeDirectoryHandle.close(); + } + + private startConnectionProcess(): void { + this.getFormData() + .pipe( + takeUntil(this.destroyed$), + switchMap(() => this.fetchParameters(this.formData)), + switchMap((params) => this.webClientService.fetchActiveDirectoryToken(params)), + switchMap((params) => from(this.activeDirectoryDataService.initializeWasm()).pipe(map(() => params))), + map((params) => this.toActiveDirectoryConnectionParams(params)), + switchMap((params) => this.callConnect(params)), + catchError((error) => { + this.handleSessionError(this.toAdConnectionError(error), true); + return EMPTY; + }), + ) + .subscribe(); + } + + private callConnect(connectionParameters: ActiveDirectoryConnectionParams): Observable { + if (!this.activeDirectoryHandle) { + return EMPTY; + } + + this.loading = true; + return this.activeDirectoryHandle.connect(connectionParameters); + } + + private getFormData(): Observable { + return from(this.webSessionService.getWebSession(this.webSessionId)).pipe( + map((currentWebSession) => { + this.formData = currentWebSession.data as ActiveDirectoryFormDataInput; + this.sessionInfoUsername = this.formData.username; + this.refreshSessionInfo(); + }), + ); + } + + private fetchParameters(formData: ActiveDirectoryFormDataInput): Observable { + const sessionId = uuidv4(); + const defaultPort = formData.useLdaps ? DefaultLdapsPort : DefaultLdapPort; + const extractedData = this.utils.string.extractHostnameAndPort( + formData.hostname, + Number(formData.port) || defaultPort, + ); + const gatewayAddress = new URL(JET_AD_URL, window.location.href).href; + + this.sessionInfoUrl = this.toUserFacingUrl(gatewayAddress); + this.sessionInfoUsername = formData.username; + this.refreshSessionInfo(); + + return of({ + host: extractedData.hostname, + port: extractedData.port, + username: formData.username, + password: formData.password, + domain: formData.domain ?? '', + gatewayAddress, + sessionId, + useLdaps: formData.useLdaps, + organizationalUnit: formData.organizationalUnit, + }); + } + + private toActiveDirectoryConnectionParams( + connectionParameters: ActiveDirectoryConnectionParameters, + ): ActiveDirectoryConnectionParams { + this.activeDirectorySessionManager.setWebSessionConfig({ + sessionId: connectionParameters.sessionId, + // Gateway standalone does not expose a DVLS gateway identifier here. + gatewayId: '', + hostname: connectionParameters.host, + port: connectionParameters.port, + username: connectionParameters.username, + password: connectionParameters.password, + domain: connectionParameters.domain, + useSSL: connectionParameters.useLdaps, + token: connectionParameters.token, + kdcProxyUrl: connectionParameters.kdcUrl, + getGatewayWebSocketUrl: (path: string) => new URL(path, window.location.href).href, + }); + + return { + connectionSettings: { + host: connectionParameters.host, + port: connectionParameters.port.toString(), + username: connectionParameters.username, + password: connectionParameters.password, + domain: connectionParameters.domain ?? '', + gatewayUrl: connectionParameters.gatewayAddress, + token: connectionParameters.token, + kdcUrl: connectionParameters.kdcUrl, + }, + isLdaps: connectionParameters.useLdaps, + sessionId: connectionParameters.sessionId, + sessionRecordingTokens: { + gatewayId: null, + recordingUrl: null, + shouldStartRecording: 'no', + }, + }; + } + + private subscribeToActiveDirectoryEvents(handle: ActiveDirectoryMainHandle): void { + this.activeDirectoryEventsSubscription?.unsubscribe(); + this.activeDirectoryEventsSubscription = handle.adConnectionEvents + .pipe(takeUntil(this.destroyed$)) + .subscribe((event) => { + switch (event.type) { + case AdConnectionEventType.Connected: + this.handleSessionStarted(); + break; + case AdConnectionEventType.Terminated: + this.handleSessionTerminated(this.activeDirectoryTranslator.translate('msgSessionTerminated')); + break; + case AdConnectionEventType.Error: + this.handleSessionError(event.error, !this.currentStatus.isInitialized); + break; + case AdConnectionEventType.Warning: + this.activeDirectoryUiService.warn(this.activeDirectoryTranslator.translate(event.message)); + break; + case AdConnectionEventType.Success: + this.activeDirectoryUiService.success(this.activeDirectoryTranslator.translate(event.message)); + break; + } + }); + } + + private handleSessionStarted(): void { + this.loading = false; + void this.webSessionService.updateWebSessionIcon(this.webSessionId, DVL_ACTIVE_DIRECTORY_ICON); + this.initializeStatus(); + this.handleConnectionSuccess(); + } + + private handleSessionTerminated(message: string): void { + this.loading = false; + + if (this.currentStatus.isDisabledByUser) { + return; + } + + this.currentStatus.terminationMessage = { + summary: message, + severity: 'error', + }; + void this.webSessionService.updateWebSessionIcon(this.webSessionId, DVL_WARNING_ICON); + this.disableComponentStatus(); + this.webClientConnectionClosed(); + } + + private handleSessionError(error: AdConnectionError, disableComponent = true): void { + this.loading = false; + const summary = this.getErrorSummary(error); + const detail = this.getErrorDetail(error, summary); + + this.activeDirectoryUiService.error(summary, detail); + console.error(detail); + + if (!disableComponent) { + return; + } + + this.currentStatus.terminationMessage = { + summary, + detail, + severity: 'error', + }; + + void this.webSessionService.updateWebSessionIcon(this.webSessionId, DVL_WARNING_ICON); + this.disableComponentStatus(); + this.webClientConnectionClosed(); + } + + private getErrorSummary(error: AdConnectionError): string { + const causeTranslationKey = this.getErrorTranslationKeyFromCause(error.cause); + if (causeTranslationKey) { + return this.activeDirectoryTranslator.translate(causeTranslationKey); + } + + if (error.code === 'other') { + return this.activeDirectoryTranslator.translate('AnUnexpectedErrorOccured'); + } + + return this.activeDirectoryTranslator.translate(matchTranslateKey(error.code)); + } + + private getErrorDetail(error: AdConnectionError, summary: string): string | undefined { + const causeMessage = typeof error.cause === 'string' ? error.cause : undefined; + const translatedMessage = this.activeDirectoryTranslator.translate(error.message); + + if (causeMessage && causeMessage !== translatedMessage) { + return causeMessage; + } + + return translatedMessage && translatedMessage !== summary ? translatedMessage : undefined; + } + + private getErrorTranslationKeyFromCause(cause: unknown): string | undefined { + if (typeof cause !== 'string') { + return undefined; + } + + const normalizedCause = cause.toLowerCase(); + + if ( + normalizedCause.includes('entry_exists') || + normalizedCause.includes('entryalreadyexists') || + normalizedCause.includes('entry already exists') || + normalizedCause.includes('problem 6005') + ) { + return 'AlreadyExists'; + } + + return undefined; + } + + private initializeStatus(): void { + this.currentStatus = { + id: this.webSessionId, + isInitialized: true, + isDisabled: false, + isDisabledByUser: false, + }; + } + + private handleConnectionSuccess(): void { + this.hideSpinnerOnly = true; + this.activeDirectoryUiService.success(this.activeDirectoryTranslator.translate('msgConnected')); + this.analyticHandle = this.analyticService.sendOpenEvent(this.getProtocol()); + } + + private disableComponentStatus(): void { + if (this.currentStatus.isDisabled) { + return; + } + + this.currentStatus.id ??= this.webSessionId; + this.currentStatus.isDisabled = true; + this.componentStatus.emit(this.currentStatus); + } + + private toAdConnectionError(error: unknown): AdConnectionError { + if (this.isAdConnectionError(error)) { + return error; + } + + return { + code: 'other', + message: error instanceof Error ? error.message : `${error}`, + cause: error, + }; + } + + private isAdConnectionError(error: unknown): error is AdConnectionError { + return ( + typeof error === 'object' && + error !== null && + 'code' in error && + 'message' in error && + typeof (error as AdConnectionError).message === 'string' + ); + } + + protected getProtocol(): ProtocolString { + return 'ActiveDirectory'; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html new file mode 100644 index 000000000..f92b72ca1 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.html @@ -0,0 +1,80 @@ +
+
+ +
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+ +
+ @if (form.get('port')?.hasError('invalidPort') && form.get('port')?.touched) { +
+ Port must be between 1 and 65535. +
+ } +
+ +
+
+ + +
+
+ +
+ +
+ +
+
+
diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss new file mode 100644 index 000000000..591d871ea --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.scss @@ -0,0 +1,17 @@ +.active-directory-form { + display: flex; + flex-direction: column; + gap: 0.75rem; + + .gateway-form-group { + margin-bottom: 0; + } + + .gateway-form-input { + margin-top: 0.25rem; + } + + .organizational-unit-form-group { + padding-bottom: 1rem; + } +} \ No newline at end of file diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts new file mode 100644 index 000000000..2d6820c26 --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/form-components/active-directory/active-directory-form.component.ts @@ -0,0 +1,96 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { AbstractControl, FormGroup, ValidatorFn } from '@angular/forms'; +import { BaseComponent } from '@shared/bases/base.component'; +import { ActiveDirectoryFormDataInput } from '@shared/interfaces/forms.interfaces'; +import { DefaultLdapPort, DefaultLdapsPort } from '@shared/services/web-client.service'; +import { WebFormService } from '@shared/services/web-form.service'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + standalone: false, + selector: 'active-directory-form', + templateUrl: 'active-directory-form.component.html', + styleUrls: ['active-directory-form.component.scss'], +}) +export class ActiveDirectoryFormComponent extends BaseComponent implements OnInit { + @Input() form: FormGroup; + @Input() inputFormData: ActiveDirectoryFormDataInput; + + constructor(private formService: WebFormService) { + super(); + } + + ngOnInit(): void { + this.addControlsToParentForm(); + this.subscribeToLdapsChanges(); + } + + private addControlsToParentForm(): void { + const useLdaps = this.inputFormData?.useLdaps ?? false; + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'domain', + inputFormData: this.inputFormData, + isRequired: false, + }); + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'port', + inputFormData: this.inputFormData, + isRequired: true, + defaultValue: useLdaps ? DefaultLdapsPort : DefaultLdapPort, + additionalValidator: this.portValidator(), + }); + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'useLdaps', + inputFormData: this.inputFormData, + isRequired: false, + defaultValue: false, + }); + + this.formService.addControlToForm({ + formGroup: this.form, + controlName: 'organizationalUnit', + inputFormData: this.inputFormData, + isRequired: false, + }); + } + + private subscribeToLdapsChanges(): void { + const useLdapsControl = this.form.get('useLdaps'); + const portControl = this.form.get('port'); + + if (!useLdapsControl || !portControl) { + return; + } + + let previousDefaultPort = useLdapsControl.value ? DefaultLdapsPort : DefaultLdapPort; + + useLdapsControl.valueChanges.pipe(takeUntil(this.destroyed$)).subscribe((useLdaps: boolean) => { + const nextDefaultPort = useLdaps ? DefaultLdapsPort : DefaultLdapPort; + const currentPort = Number(portControl.value); + + if (currentPort === previousDefaultPort) { + portControl.setValue(nextDefaultPort); + } + + previousDefaultPort = nextDefaultPort; + }); + } + + private portValidator(): ValidatorFn { + return (control: AbstractControl): { [key: string]: unknown } | null => { + const port = Number(control.value); + + if (!Number.isInteger(port) || port < 1 || port > 65535) { + return { invalidPort: { value: control.value } }; + } + + return null; + }; + } +} diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html index 1fd6391d7..747260805 100644 --- a/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/form/web-client-form.component.html @@ -48,7 +48,7 @@
- +
} + @if (isSelectedProtocolActiveDirectory()) { + + } +
> = { + [Protocol.ActiveDirectory]: 'Domain Controller', + }; + constructor( private fb: FormBuilder, private formService: WebFormService, @@ -248,6 +252,14 @@ export class WebClientFormComponent extends BaseSessionComponent implements OnIn return WebClientProtocol.isProtocolArd(this.getSelectedProtocol()); } + isSelectedProtocolActiveDirectory(): boolean { + return WebClientProtocol.isProtocolActiveDirectory(this.getSelectedProtocol()); + } + + getHostnameLabel(): string { + return this.hostnameLabels[this.getSelectedProtocol()] ?? 'Hostname'; + } + private addMessages(newMessages: ToastMessageOptions[]): void { const areThereNewMessages: boolean = newMessages.some( (newMsg) => diff --git a/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts b/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts index 63c2fbf52..97c4aed91 100644 --- a/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts +++ b/webapp/apps/gateway-ui/src/client/app/modules/web-client/web-client.module.ts @@ -2,8 +2,10 @@ import { NgOptimizedImage } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; +import { WebActiveDirectoryComponent } from '@devolutions/web-active-directory-gui'; import { ArdToolbarWrapperComponent } from '@gateway/modules/web-client/ard/ard-toolbar-wrapper.component'; import { WebClientArdComponent } from '@gateway/modules/web-client/ard/web-client-ard.component'; +import { ActiveDirectoryFormComponent } from '@gateway/modules/web-client/form/form-components/active-directory/active-directory-form.component'; import { ArdFormComponent } from '@gateway/modules/web-client/form/form-components/ard/ard-form.component'; import { RdpFormComponent } from '@gateway/modules/web-client/form/form-components/rdp/rdp-form.component'; import { SshFormComponent } from '@gateway/modules/web-client/form/form-components/ssh/ssh-form.component'; @@ -31,6 +33,7 @@ import { WebSessionService } from '@shared/services/web-session.service'; import { SharedModule } from '@shared/shared.module'; import { CheckboxModule } from 'primeng/checkbox'; import { KeyFilterModule } from 'primeng/keyfilter'; +import { WebClientActiveDirectoryComponent } from './active-directory/web-client-active-directory.component'; import { ArdQualityModeControlComponent } from './form/form-controls/ard-quality-mode-control/ard-quality-mode-control.component'; import { AutoClipboardControlComponent } from './form/form-controls/auto-clipboard-control/auto-clipboard-control.component'; import { ColorFormatControlComponent } from './form/form-controls/color-format-control/color-format-control.component'; @@ -72,6 +75,7 @@ const routes: Routes = [ ArdToolbarWrapperComponent, RdpToolbarWrapperComponent, VncToolbarWrapperComponent, + WebActiveDirectoryComponent, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], declarations: [ @@ -82,11 +86,13 @@ const routes: Routes = [ WebClientTelnetComponent, WebClientSshComponent, WebClientArdComponent, + WebClientActiveDirectoryComponent, WebClientFormComponent, RdpFormComponent, SshFormComponent, VncFormComponent, ArdFormComponent, + ActiveDirectoryFormComponent, UsernameControlComponent, PasswordControlComponent, ExtendedClipboardControlComponent, @@ -114,7 +120,6 @@ const routes: Routes = [ NetScanComponent, ], exports: [DynamicTabComponent, WebClientFormComponent, NetScanComponent], - providers: [], }) export class WebClientModule { constructor(webSessionService: WebSessionService) { @@ -124,6 +129,7 @@ export class WebClientModule { [Protocol.SSH]: WebClientSshComponent, [Protocol.VNC]: WebClientVncComponent, [Protocol.ARD]: WebClientArdComponent, + [Protocol.ActiveDirectory]: WebClientActiveDirectoryComponent, }); } } diff --git a/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts b/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts index 516e675e6..3b119b2a6 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/enums/web-client-protocol.enum.ts @@ -6,6 +6,7 @@ export enum Protocol { SSH = 2, VNC = 3, ARD = 4, + ActiveDirectory = 5, } enum Tooltips { @@ -14,11 +15,16 @@ enum Tooltips { 'Secure Shell' = 'SSH', 'Virtual Network Computing' = 'VNC', 'Apple Remote Desktop' = 'ARD', + 'Active Directory' = 'ActiveDirectory', } export type ProtocolControlMap = { [key in Protocol]?: string[] }; namespace WebClientProtocol { + const DisplayNames: Partial> = { + ActiveDirectory: 'Active Directory', + }; + export function getEnumKey(value: Protocol): string { return Protocol[value]; } @@ -33,7 +39,7 @@ namespace WebClientProtocol { return Object.keys(Protocol) .filter((key) => Number.isNaN(Number(key)) && typeof Protocol[key] === 'number') .map((key) => { - const label: string = key; + const label: string = DisplayNames[key as keyof typeof Protocol] ?? key; const value: Protocol = Protocol[key as keyof typeof Protocol]; const tooltipText = tooltipsLookup[key] || ''; @@ -56,5 +62,9 @@ namespace WebClientProtocol { export function isProtocolArd(protocol: Protocol): boolean { return protocol === Protocol.ARD; } + + export function isProtocolActiveDirectory(protocol: Protocol): boolean { + return protocol === Protocol.ActiveDirectory; + } } export { WebClientProtocol }; diff --git a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts index 354301157..64d2e5929 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/connection-params.interfaces.ts @@ -77,3 +77,37 @@ export interface SshConnectionParameters { privateKey?: string; privateKeyPassphrase?: string; } + +export interface ActiveDirectoryConnectionParameters { + host: string; + port: number; + username: string; + password: string; + domain?: string; + gatewayAddress: string; + token?: string; + kdcUrl?: string; + sessionId?: string; + useLdaps: boolean; + organizationalUnit?: string; +} + +export function isActiveDirectoryConnectionParameters(params: unknown): params is ActiveDirectoryConnectionParameters { + if (typeof params !== 'object' || params === null) { + return false; + } + + const candidate = params as Partial; + + return ( + typeof candidate.host === 'string' && + typeof candidate.port === 'number' && + Number.isInteger(candidate.port) && + candidate.port >= 1 && + candidate.port <= 65535 && + typeof candidate.username === 'string' && + typeof candidate.password === 'string' && + typeof candidate.gatewayAddress === 'string' && + typeof candidate.useLdaps === 'boolean' + ); +} diff --git a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts index 4b31e97fd..ec9a18233 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/interfaces/forms.interfaces.ts @@ -19,7 +19,8 @@ export type FormDataUnion = | VncFormDataInput | ArdFormDataInput | SSHFormDataInput - | TelnetFormDataInput; + | TelnetFormDataInput + | ActiveDirectoryFormDataInput; export interface RdpFormDataInput { autoComplete: AutoCompleteInput; @@ -86,3 +87,15 @@ export interface SSHFormDataInput { sshPrivateKey?: string; }; } + +export interface ActiveDirectoryFormDataInput { + autoComplete: AutoCompleteInput; + hostname: string; + username: string; + password: string; + domain?: string; + port: number; + useLdaps: boolean; + organizationalUnit?: string; + protocol: number; +} diff --git a/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts b/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts index b96fa403d..617154c4f 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/models/web-session.model.ts @@ -1,4 +1,5 @@ import { ComponentRef, ElementRef, Type } from '@angular/core'; +import type { WebClientActiveDirectoryComponent } from '@gateway/modules/web-client/active-directory/web-client-active-directory.component'; import type { WebClientArdComponent } from '@gateway/modules/web-client/ard/web-client-ard.component'; import type { WebClientFormComponent } from '@gateway/modules/web-client/form/web-client-form.component'; import type { WebClientRdpComponent } from '@gateway/modules/web-client/rdp/web-client-rdp.component'; @@ -11,6 +12,7 @@ import { DesktopSize } from '@shared/models/desktop-size'; import { v4 as uuidv4 } from 'uuid'; import { BaseComponent } from '../bases/base.component'; import { + ActiveDirectoryFormDataInput, ArdFormDataInput, RdpFormDataInput, SSHFormDataInput, @@ -19,6 +21,7 @@ import { } from '../interfaces/forms.interfaces'; export type WebSessionComponentType = + | Type | Type | Type | Type @@ -30,6 +33,7 @@ export type WebSessionComponentType = export interface SessionDataTypeMap { WebClientFormComponent: never; MainPanelComponent: never; + WebClientActiveDirectoryComponent: ActiveDirectoryFormDataInput; WebClientArdComponent: ArdFormDataInput; WebClientRdpComponent: RdpFormDataInput; WebClientSshComponent: SSHFormDataInput; @@ -38,6 +42,7 @@ export interface SessionDataTypeMap { } export interface SessionTypeMap { + WebClientActiveDirectoryComponent: WebClientActiveDirectoryComponent; WebClientArdComponent: WebClientArdComponent; WebClientFormComponent: WebClientFormComponent; WebClientRdpComponent: WebClientRdpComponent; diff --git a/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts b/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts index fe00ce865..2230be0d1 100644 --- a/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts +++ b/webapp/apps/gateway-ui/src/client/app/shared/services/web-client.service.ts @@ -4,9 +4,11 @@ import { BaseComponent } from '@shared/bases/base.component'; import { ScreenSize } from '@shared/enums/screen-size.enum'; import { Protocol, WebClientProtocol } from '@shared/enums/web-client-protocol.enum'; import { + ActiveDirectoryConnectionParameters, IronARDConnectionParameters, IronRDPConnectionParameters, IronVNCConnectionParameters, + isActiveDirectoryConnectionParameters, SessionTokenParameters, SshConnectionParameters, TelnetConnectionParameters, @@ -28,6 +30,8 @@ export const DefaultSshPort: number = 22; export const DefaultTelnetPort: number = 23; export const DefaultVncPort: number = 5900; export const DefaultArdPort: number = 5900; +export const DefaultLdapPort: number = 389; +export const DefaultLdapsPort: number = 636; @Injectable() export class WebClientService extends BaseComponent { @@ -75,18 +79,46 @@ export class WebClientService extends BaseComponent { return DefaultVncPort; case Protocol.ARD: return DefaultArdPort; + case Protocol.ActiveDirectory: + return DefaultLdapPort; default: throw new Error(`Getting default port, unsupported protocol: ${protocol}`); } } + private getApplicationProtocol( + protocol: Protocol, + connectionParameters: + | TelnetConnectionParameters + | SshConnectionParameters + | IronRDPConnectionParameters + | IronVNCConnectionParameters + | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters, + ): string { + if (protocol === Protocol.ActiveDirectory) { + if (!isActiveDirectoryConnectionParameters(connectionParameters)) { + console.error('invalid active directory connection settings'); + + const error = new Error('errConnectionFailed') as Error & { code: string }; + error.code = 'errConnectionFailed'; + throw error; + } + + return connectionParameters.useLdaps ? 'ldaps' : 'ldap'; + } + + return WebClientProtocol.getEnumKey(protocol).toLowerCase(); + } + private handleProtocolTokenRequest< T extends | TelnetConnectionParameters | SshConnectionParameters | IronRDPConnectionParameters | IronVNCConnectionParameters - | IronARDConnectionParameters, + | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters, >(protocol: Protocol, connectionParameters: T): Observable { return this.generateProtocolToken(protocol, connectionParameters).pipe( takeUntil(this.destroyed$), @@ -102,15 +134,17 @@ export class WebClientService extends BaseComponent { | SshConnectionParameters | IronRDPConnectionParameters | IronVNCConnectionParameters - | IronARDConnectionParameters, + | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters, ): Observable< | TelnetConnectionParameters | SshConnectionParameters | IronRDPConnectionParameters | IronVNCConnectionParameters | IronARDConnectionParameters + | ActiveDirectoryConnectionParameters > { - const protocolStr: string = WebClientProtocol.getEnumKey(protocol).toLowerCase(); + const protocolStr: string = this.getApplicationProtocol(protocol, connectionParameters); const data: SessionTokenParameters = { content_type: 'ASSOCIATION', @@ -151,6 +185,12 @@ export class WebClientService extends BaseComponent { return this.handleProtocolTokenRequest(Protocol.ARD, connectionParameters); } + fetchActiveDirectoryToken( + connectionParameters: ActiveDirectoryConnectionParameters, + ): Observable { + return this.handleProtocolTokenRequest(Protocol.ActiveDirectory, connectionParameters); + } + fetchNetScanToken(): Observable { const data: SessionTokenParameters = { content_type: 'NETSCAN', diff --git a/webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss b/webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss new file mode 100644 index 000000000..c2b90758e --- /dev/null +++ b/webapp/apps/gateway-ui/src/client/app/styles/ad-theme-bridge.gateway.scss @@ -0,0 +1,254 @@ +:where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog +) { + --ad-color-primary: var(--blue-primary); + --ad-on-primary: #fff; + --ad-text-primary: var(--text-color-primary); + --ad-text-secondary: var(--text-color-secondary); + --ad-text-disabled: var(--grey-600); + --ad-border: var(--border-secondary-color); + --ad-border-color: var(--border-secondary-color); + --ad-input-border: var(--input-border-color); + --ad-bg: var(--connection-view-default-background-color); + --ad-bg-muted: var(--background-secondary-color); + --ad-error: var(--message-error-color); + --ad-toolbar-bg: var(--connection-view-default-background-color); + --ad-title: var(--title-color); + --ad-accent-text: var(--tree-selector-text-highlight-color); + --ad-splitter: var(--border-secondary-color); + --ad-tree-item-highlight-bg: var(--main-tree-item-highlight-color, #e7f4ff); + --ad-tab-list-border-color: var(--dialog-menu-border-color); + --ad-tab-text-color: var(--dialog-menu-text-color); + --ad-tab-hover-text-color: var(--dialog-menu-highlight-text-color); + --ad-tab-active-color: var(--fieldset-title-color); + --ad-tab-nav-button-color: var(--ad-tab-text-color); + --ad-tab-nav-button-hover-color: var(--ad-tab-hover-text-color); + --ad-table-border-color: var(--table-border-color); + --ad-table-header-bg: #f9f9f9; + --ad-table-header-hover-bg: #f2f2f2; + --ad-table-header-text: #404040; + --ad-table-row-text: var(--table-row-text-color); + --ad-table-row-bg: var(--table-row-background-color); + --ad-table-row-alt-bg: var(--table-child-row-background-color); + --ad-table-row-hover-bg: var(--hover-bg-color); + --ad-table-results-text: var(--table-row-text-color); + --ad-table-action-color: var(--table-row-text-color); + --ad-table-action-hover-bg: var(--hover-bg-color); + --ad-table-action-hover-color: var(--table-link-color); + --ad-attributes-grid-row-bg: var(--table-row-background-color); + --ad-attributes-grid-row-hover-bg: var(--table-row-background-color); + --ad-attributes-grid-header-bg: #f9f9f9; + --ad-attributes-grid-header-hover-bg: #f2f2f2; + --ad-attributes-grid-header-border-color: #f9f9f9; + --ad-attributes-grid-border-color: var(--table-border-color); + --ad-attributes-grid-text-color: var(--table-row-text-color); + --ad-attributes-grid-header-text-color: #404040; + --ad-attributes-grid-icon-color: var(--btn-icon-color, var(--table-row-text-color)); + --ad-attributes-grid-icon-hover-color: var(--btn-icon-color, var(--table-link-color)); + --ad-members-action-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-action-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-members-search-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-search-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-attributes-grid-paginator-page-bg: transparent; + --ad-attributes-grid-paginator-page-color: var(--table-row-text-color); + --ad-attributes-grid-paginator-page-selected-bg: var(--dropdown-item-highlight-background-color, #21374f); + --ad-attributes-grid-paginator-page-selected-color: var(--table-link-color, #3aa3ff); + --ad-attributes-grid-paginator-dropdown-bg: transparent; + --ad-attributes-grid-paginator-dropdown-color: var(--text-color-primary, #fff); + --ad-attributes-grid-paginator-dropdown-border-color: var(--input-border-color, var(--table-border-color)); + --ad-attributes-grid-paginator-dropdown-border-width: 0.8px; + --ad-attributes-grid-paginator-dropdown-border-radius: 8px; + --ad-attributes-grid-results-text: var(--table-column-title-color, var(--text-color-secondary)); + --ad-tree-bg: var(--connection-view-default-background-color, var(--background-secondary-color)); + --ad-tree-menu-bg: var(--panel-menu-background-color, var(--ad-tree-bg)); + --ad-tree-menu-item-color: var(--dialog-menu-text-color, var(--text-color-secondary)); + --ad-tree-menu-item-hover-bg: var(--panel-menu-hover-background-color, var(--hover-bg-color)); + --ad-tree-menu-item-active-bg: var(--panel-menu-highlight-background-color, var(--ad-tree-item-highlight-bg)); + --ad-tree-menu-item-active-color: var(--panel-menu-highlight-text-color, var(--ad-accent-text)); + --ad-tree-menu-muted-color: var(--text-color-secondary, var(--ad-text-secondary)); + --ad-font-family: var(--font-open-sans, 'Open Sans'); + --ad-accent-rgb: var(--base-accent-rgb); + --ad-sensitive: var(--sensitive-color, #ffa500); + --ad-label-readonly: var(--label-readonly-color); + + /* Button colors */ + --ad-button-primary-bg: var(--dialog-footer-default-button-background-color, var(--blue-primary)); + --ad-button-primary-bg-hover: var(--dialog-footer-default-button-background-hover-color, var(--blue-800)); + --ad-button-primary-text: var(--dialog-footer-default-button-text-color, var(--blue-primary-text-color, #fff)); + --ad-button-cancel-bg: var(--bg-100, #f2f7fc); + --ad-button-cancel-bg-hover: var(--bg-200, #e6edf5); + --ad-button-cancel-border-color: var(--border-neutral-200, #c2c3c6); + --ad-button-cancel-border-color-hover: var(--border-neutral-200, #c2c3c6); + --ad-button-cancel-border-width: 0.8px; + --ad-button-cancel-text: var(--text-brand, #0068c3); + + /* Secondary button — light */ + --ad-button-secondary-bg: var(--button-secondary-fill, color-mix(in srgb, var(--ad-color-primary) 10%, white)); + --ad-button-secondary-bg-hover: var(--button-secondary-hover-fill, color-mix(in srgb, var(--ad-color-primary) 18%, white)); + --ad-button-secondary-border-color: var(--button-secondary-border, color-mix(in srgb, var(--ad-color-primary) 30%, transparent)); + --ad-button-secondary-border-color-hover: var(--button-secondary-hover-border, color-mix(in srgb, var(--ad-color-primary) 50%, transparent)); + --ad-button-secondary-border-width: 1px; + --ad-button-secondary-text: var(--button-secondary-text, var(--ad-color-primary)); +} + +html[data-theme='dark'] + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ), +body[data-theme='dark'] + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ), +html.dark-theme + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ), +body.dark-theme + :where( + [data-ad-theme-bridge], + .ad-bitlocker-search, + .ad-dvls-confirm-dialog, + .ad-entry-properties-modal, + .ad-new-group-modal, + .ad-new-user-modal, + .ad-update-password-modal, + .edit-boolean-dialog, + .edit-list-dialog, + .edit-number-dialog, + .edit-string-dialog, + .new-members-dialog + ) { + --connection-view-default-background-color: #0c111d; + --ad-color-primary: var(--blue-800); + --ad-on-primary: #fff; + --ad-text-primary: var(--text-color-primary); + --ad-text-secondary: var(--text-color-secondary); + --ad-text-disabled: var(--grey-600); + --ad-border: var(--border-secondary-color); + --ad-border-color: var(--border-secondary-color); + --ad-input-border: var(--input-border-color); + --ad-bg: #0c111d; + --ad-bg-muted: var(--background-secondary-color); + --ad-error: var(--message-error-color); + --ad-toolbar-bg: var(--connection-view-default-background-color); + --ad-title: var(--title-color); + --ad-accent-text: var(--tree-selector-text-highlight-color); + --ad-splitter: var(--border-secondary-color); + --ad-tree-item-highlight-bg: var(--main-tree-item-highlight-color, #143857); + --ad-tab-list-border-color: var(--dialog-menu-border-color); + --ad-tab-text-color: var(--dialog-menu-text-color); + --ad-tab-hover-text-color: var(--dialog-menu-highlight-text-color); + --ad-tab-active-color: var(--fieldset-title-color); + --ad-tab-nav-button-color: var(--ad-tab-text-color); + --ad-tab-nav-button-hover-color: var(--ad-tab-hover-text-color); + --ad-table-border-color: var(--table-border-color); + --ad-table-header-bg: #0c111d; + --ad-table-header-hover-bg: #1c222e; + --ad-table-header-text: var(--table-column-title-color); + --ad-table-row-text: var(--table-row-text-color); + --ad-table-row-bg: var(--table-row-background-color); + --ad-table-row-alt-bg: var(--table-child-row-background-color); + --ad-table-row-hover-bg: var(--hover-bg-color); + --ad-table-results-text: var(--table-row-text-color); + --ad-table-action-color: var(--table-row-text-color); + --ad-table-action-hover-bg: var(--hover-bg-color); + --ad-table-action-hover-color: var(--table-link-color); + --ad-attributes-grid-row-bg: #161b26; + --ad-attributes-grid-row-hover-bg: #161b26; + --ad-attributes-grid-header-bg: #0c111d; + --ad-attributes-grid-header-hover-bg: #1c222e; + --ad-attributes-grid-header-border-color: #0c111d; + --ad-attributes-grid-border-color: #2a2e39; + --ad-attributes-grid-text-color: #c2c3c6; + --ad-attributes-grid-header-text-color: #c2c3c6; + --ad-attributes-grid-icon-color: var(--btn-icon-color, var(--table-row-text-color)); + --ad-attributes-grid-icon-hover-color: var(--btn-icon-color, var(--table-link-color)); + --ad-members-action-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-action-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-members-search-icon-color: var(--ad-attributes-grid-icon-color); + --ad-members-search-icon-hover-color: var(--ad-attributes-grid-icon-hover-color); + --ad-attributes-grid-paginator-page-bg: transparent; + --ad-attributes-grid-paginator-page-color: #c2c3c6; + --ad-attributes-grid-paginator-page-selected-bg: #21374f; + --ad-attributes-grid-paginator-page-selected-color: #3aa3ff; + --ad-attributes-grid-paginator-dropdown-bg: #0c111d; + --ad-attributes-grid-paginator-dropdown-color: #96989d; + --ad-attributes-grid-paginator-dropdown-border-color: #2f3441; + --ad-attributes-grid-paginator-dropdown-border-width: 0.8px; + --ad-attributes-grid-paginator-dropdown-border-radius: 8px; + --ad-attributes-grid-results-text: #96989d; + --ad-tree-bg: var(--background-secondary-color, var(--connection-view-default-background-color)); + --ad-tree-menu-bg: #0c111d; + --ad-tree-menu-item-color: #96989d; + --ad-tree-menu-item-hover-bg: #0f1f33; + --ad-tree-menu-item-active-bg: #102a45; + --ad-tree-menu-item-active-color: #3aa3ff; + --ad-tree-menu-muted-color: #96989d; + --ad-font-family: var(--font-open-sans, 'Open Sans'); + --ad-accent-rgb: var(--base-accent-rgb); + --ad-sensitive: var(--sensitive-color, #ffa500); + --ad-label-readonly: var(--label-readonly-color); + + /* Button colors */ + --ad-button-primary-bg: var(--dialog-footer-default-button-background-color, var(--blue-primary)); + --ad-button-primary-bg-hover: var(--dialog-footer-default-button-background-hover-color, var(--blue-800)); + --ad-button-primary-text: var(--dialog-footer-default-button-text-color, var(--blue-primary-text-color, #fff)); + --ad-button-cancel-bg: #161b26; + --ad-button-cancel-bg-hover: #1c222e; + --ad-button-cancel-border-color: #2f3441; + --ad-button-cancel-border-color-hover: #2f3441; + --ad-button-cancel-border-width: 0.8px; + --ad-button-cancel-text: #3aa3ff; + + /* Secondary button — exact values from computed dvl-button[variant=secondary] dark */ + --ad-button-secondary-bg: #1a2b40; + --ad-button-secondary-bg-hover: #1f3552; + --ad-button-secondary-border-color: rgba(58, 163, 255, 0.15); + --ad-button-secondary-border-color-hover: rgba(58, 163, 255, 0.28); + --ad-button-secondary-border-width: 0.8px; + --ad-button-secondary-text: #3aa3ff; +} diff --git a/webapp/pnpm-lock.yaml b/webapp/pnpm-lock.yaml index 07e635fa9..76c231983 100644 --- a/webapp/pnpm-lock.yaml +++ b/webapp/pnpm-lock.yaml @@ -69,9 +69,15 @@ importers: '@devolutions/iron-remote-desktop-vnc': specifier: 0.7.0 version: 0.7.0 + '@devolutions/ldap-wasm-js': + specifier: ^0.3.4 + version: 0.3.4 '@devolutions/terminal-shared': specifier: ^1.4.1 version: 1.4.1(typescript@5.9.3) + '@devolutions/web-active-directory-gui': + specifier: 0.0.13 + version: 0.0.13(ffa535f5f8fdad6835c93e9f6c4f1c9b) '@devolutions/web-ssh-gui': specifier: ^0.7.0 version: 0.7.0(@devolutions/terminal-shared@1.4.1(typescript@5.9.3)) @@ -85,11 +91,11 @@ importers: specifier: ^1.2.5 version: 1.2.5 '@primeuix/themes': - specifier: ^1.2.5 - version: 1.2.5 + specifier: 2.0.3 + version: 2.0.3 '@primeuix/utils': - specifier: ^0.6.3 - version: 0.6.3 + specifier: 0.7.2 + version: 0.7.2 '@xterm/addon-clipboard': specifier: ^0.1.0 version: 0.1.0(@xterm/xterm@5.5.0) @@ -448,6 +454,7 @@ packages: '@angular/animations@20.3.18': resolution: {integrity: sha512-XFxgSyjfs0SRD2vQVFJljmM4z9nTvUoI8TRqSre/+l8D2FgzD5pG67Aj2BgDgpSFAUkIcI37G48ijK7a3ZZ3WA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: '@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.' peerDependencies: '@angular/core': 20.3.18 @@ -556,6 +563,7 @@ packages: '@angular/platform-browser-dynamic@20.3.18': resolution: {integrity: sha512-NyTobOGYVzGmPmtI+3lxMzxi0TbLq4SRNQ2ENEJAt6k2JnMmHBm483ppLRAM47nGlDdiraW0IX93EtYYNkiK3g==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + deprecated: '@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.' peerDependencies: '@angular/common': 20.3.18 '@angular/compiler': 20.3.18 @@ -1238,48 +1246,56 @@ packages: engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64-musl@2.1.3': resolution: {integrity: sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [musl] '@biomejs/cli-linux-arm64@1.9.4': resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-arm64@2.1.3': resolution: {integrity: sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64-musl@1.9.4': resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64-musl@2.1.3': resolution: {integrity: sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [musl] '@biomejs/cli-linux-x64@1.9.4': resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-linux-x64@2.1.3': resolution: {integrity: sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] + libc: [glibc] '@biomejs/cli-win32-arm64@1.9.4': resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==} @@ -1329,9 +1345,33 @@ packages: '@devolutions/iron-remote-desktop@0.11.0': resolution: {integrity: sha512-ZH8xfabDA+6rAXkW2V3XgRdj0LHOsHoXAzars/H22DmKsHM39LdMfheKFUR3qLaFDM8mjf6DTFngzjgr+Byx0Q==} + '@devolutions/ldap-wasm-js@0.3.4': + resolution: {integrity: sha512-9gI9j7yGL2MKEG1wEQExP6AF8ON31T1lGsmMGVmccbouXOoHI76ti7rqhkqYotTB0bDHA1yO9ljzVr80lXwubg==, tarball: https://devolutions.jfrog.io/devolutions/api/npm/npm/@devolutions/ldap-wasm-js/-/@devolutions/ldap-wasm-js-0.3.4.tgz} + '@devolutions/terminal-shared@1.4.1': resolution: {integrity: sha512-GLfuO3/sLKXwxvFczTPe+QSF8r2htV/2uvdfkxg4NzunFB9y4vLOnNjndCzN+1R5p0H1tv0wEnGTFcKbJQdvsQ==} + '@devolutions/web-active-directory-gui@0.0.13': + resolution: {integrity: sha512-2PxeVcXUrwowlqndjygOWBGZcvVgpElMrVZa97GSeEKbM5SXh8w10qjOweNWoiGsa154RkyfUBv7EMbfMADNmA==, tarball: https://devolutions.jfrog.io/devolutions/api/npm/npm/@devolutions/web-active-directory-gui/-/@devolutions/web-active-directory-gui-0.0.13.tgz} + peerDependencies: + '@angular/animations': 20.3.18 + '@angular/cdk': '>=19.2.19 <21.0.0' + '@angular/common': 20.3.18 + '@angular/core': 20.3.18 + '@angular/forms': 20.3.18 + '@angular/platform-browser': 20.3.18 + '@angular/platform-browser-dynamic': 20.3.18 + '@angular/router': 20.3.18 + '@devolutions/icons': ^5.0.7 + '@primeuix/styled': 0.7.4 + '@primeuix/themes': 2.0.3 + primeng: '>=19.1.4 <21.0.0' + rxjs: ~7.8.0 + zone.js: 0.15.0 + peerDependenciesMeta: + primeng: + optional: true + '@devolutions/web-ssh-gui@0.7.0': resolution: {integrity: sha512-OBXbCUMLBFknC7LisYk2iN0RSUJelRIxpJhHZiR7syOOZ/2Ga4+LlNWAH8gwwDoGyGpztgJbqHQzyEQWa6urAQ==} peerDependencies: @@ -2299,42 +2339,49 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-arm64-musl@1.1.1': resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@napi-rs/nice-linux-ppc64-gnu@1.1.1': resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-riscv64-gnu@1.1.1': resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-s390x-gnu@1.1.1': resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-gnu@1.1.1': resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@napi-rs/nice-linux-x64-musl@1.1.1': resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@napi-rs/nice-openharmony-arm64@1.1.1': resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} @@ -2450,36 +2497,42 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.1': resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] + libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.1': resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.1': resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] + libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.1': resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.1': resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] + libc: [musl] '@parcel/watcher-win32-arm64@2.5.1': resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} @@ -2510,11 +2563,15 @@ packages: '@primeuix/styles@1.2.5': resolution: {integrity: sha512-nypFRct/oaaBZqP4jinT0puW8ZIfs4u+l/vqUFmJEPU332fl5ePj6DoOpQgTLzo3OfmvSmz5a5/5b4OJJmmi7Q==} - '@primeuix/themes@1.2.5': - resolution: {integrity: sha512-n3YkwJrHQaEESc/D/A/iD815sxp8cKnmzscA6a8Tm8YvMtYU32eCahwLLe6h5rywghVwxASWuG36XBgISYOIjQ==} + '@primeuix/themes@2.0.3': + resolution: {integrity: sha512-3fS1883mtCWhgUgNf/feiaaDSOND4EBIOu9tZnzJlJ8QtYyL6eFLcA6V3ymCWqLVXQ1+lTVEZv1gl47FIdXReg==} + + '@primeuix/utils@0.6.4': + resolution: {integrity: sha512-pZ5f+vj7wSzRhC7KoEQRU5fvYAe+RP9+m39CTscZ3UywCD1Y2o6Fe1rRgklMPSkzUcty2jzkA0zMYkiJBD1hgg==} + engines: {node: '>=12.11.0'} - '@primeuix/utils@0.6.3': - resolution: {integrity: sha512-/SLNQSKQ73WbBIsflKVqbpVjCfFYvQO3Sf1LMheXyxh8JqxO4M63dzP56wwm9OPGuCQ6MYOd2AHgZXz+g7PZcg==} + '@primeuix/utils@0.7.2': + resolution: {integrity: sha512-pmEbSfP0Phf9W9RweiM66zXnkn73ZeKyYINElbX3uZ2+stzzaba2svLAl3B1pHVcRw5t43O0VciaGe4ye2EXKw==} engines: {node: '>=12.11.0'} '@rolldown/pluginutils@1.0.0-beta.27': @@ -2593,121 +2650,145 @@ packages: resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-gnueabihf@4.59.0': resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.53.5': resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.53.5': resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.53.5': resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.53.5': resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.53.5': resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.53.5': resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.53.5': resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.53.5': resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.53.5': resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.53.5': resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -2869,24 +2950,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.18': resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.18': resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.18': resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.18': resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==} @@ -5053,24 +5138,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.2: resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.2: resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.2: resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.2: resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} @@ -8184,6 +8273,8 @@ snapshots: '@devolutions/iron-remote-desktop@0.11.0': {} + '@devolutions/ldap-wasm-js@0.3.4': {} + '@devolutions/terminal-shared@1.4.1(typescript@5.9.3)': dependencies: '@xterm/addon-fit': 0.10.0(@xterm/xterm@5.5.0) @@ -8193,6 +8284,25 @@ snapshots: transitivePeerDependencies: - typescript + '@devolutions/web-active-directory-gui@0.0.13(ffa535f5f8fdad6835c93e9f6c4f1c9b)': + dependencies: + '@angular/animations': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/cdk': 20.2.14(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/common': 20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/forms': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/platform-browser': 20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/platform-browser-dynamic': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@20.3.18)(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))) + '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@devolutions/icons': 5.0.11(@types/react@19.2.7) + '@primeuix/styled': 0.7.4 + '@primeuix/themes': 2.0.3 + rxjs: 7.8.2 + tslib: 2.6.1 + zone.js: 0.15.0 + optionalDependencies: + primeng: 20.3.0(d23c4438b3ed4ab76af9fea6d9f471d1) + '@devolutions/web-ssh-gui@0.7.0(@devolutions/terminal-shared@1.4.1(typescript@5.9.3))': dependencies: '@devolutions/terminal-shared': 1.4.1(typescript@5.9.3) @@ -9273,17 +9383,19 @@ snapshots: '@primeuix/styled@0.7.4': dependencies: - '@primeuix/utils': 0.6.3 + '@primeuix/utils': 0.6.4 '@primeuix/styles@1.2.5': dependencies: '@primeuix/styled': 0.7.4 - '@primeuix/themes@1.2.5': + '@primeuix/themes@2.0.3': dependencies: '@primeuix/styled': 0.7.4 - '@primeuix/utils@0.6.3': {} + '@primeuix/utils@0.6.4': {} + + '@primeuix/utils@0.7.2': {} '@rolldown/pluginutils@1.0.0-beta.27': {} @@ -13010,7 +13122,7 @@ snapshots: '@angular/router': 20.3.18(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@20.3.18(@angular/animations@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@20.3.18(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@20.3.18(@angular/compiler@20.3.18)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@primeuix/styled': 0.7.4 '@primeuix/styles': 1.2.5 - '@primeuix/utils': 0.6.3 + '@primeuix/utils': 0.6.4 rxjs: 7.8.2 tslib: 2.6.1