diff --git a/apps/browser/src/autofill/services/dom-query.service.ts b/apps/browser/src/autofill/services/dom-query.service.ts index 570027b2d12..3ab6c6771cb 100644 --- a/apps/browser/src/autofill/services/dom-query.service.ts +++ b/apps/browser/src/autofill/services/dom-query.service.ts @@ -7,6 +7,27 @@ import { DomQueryService as DomQueryServiceInterface } from "./abstractions/dom- export class DomQueryService implements DomQueryServiceInterface { private pageContainsShadowDom: boolean; private useTreeWalkerStrategyFlagSet = true; + private ignoredTreeWalkerNodes = new Set([ + "svg", + "script", + "noscript", + "head", + "style", + "link", + "meta", + "title", + "base", + "img", + "picture", + "video", + "audio", + "object", + "source", + "track", + "param", + "map", + "area", + ]); constructor() { void this.init(); @@ -21,6 +42,7 @@ export class DomQueryService implements DomQueryServiceInterface { * @param treeWalkerFilter - The filter callback to use for the treeWalker query * @param mutationObserver - The MutationObserver to use for observing shadow roots * @param forceDeepQueryAttempt - Whether to force a deep query attempt + * @param ignoredTreeWalkerNodesOverride - An optional set of node names to ignore when using the treeWalker strategy */ query( root: Document | ShadowRoot | Element, @@ -28,15 +50,28 @@ export class DomQueryService implements DomQueryServiceInterface { treeWalkerFilter: CallableFunction, mutationObserver?: MutationObserver, forceDeepQueryAttempt?: boolean, + ignoredTreeWalkerNodesOverride?: Set, ): T[] { + const ignoredTreeWalkerNodes = ignoredTreeWalkerNodesOverride || this.ignoredTreeWalkerNodes; + if (!forceDeepQueryAttempt && this.pageContainsShadowDomElements()) { - return this.queryAllTreeWalkerNodes(root, treeWalkerFilter, mutationObserver); + return this.queryAllTreeWalkerNodes( + root, + treeWalkerFilter, + ignoredTreeWalkerNodes, + mutationObserver, + ); } try { return this.deepQueryElements(root, queryString, mutationObserver); } catch { - return this.queryAllTreeWalkerNodes(root, treeWalkerFilter, mutationObserver); + return this.queryAllTreeWalkerNodes( + root, + treeWalkerFilter, + ignoredTreeWalkerNodes, + mutationObserver, + ); } } @@ -207,11 +242,13 @@ export class DomQueryService implements DomQueryServiceInterface { * and returns a collection of nodes. * @param rootNode * @param filterCallback + * @param ignoredTreeWalkerNodes * @param mutationObserver */ private queryAllTreeWalkerNodes( rootNode: Node, filterCallback: CallableFunction, + ignoredTreeWalkerNodes: Set, mutationObserver?: MutationObserver, ): T[] { const treeWalkerQueryResults: T[] = []; @@ -220,6 +257,7 @@ export class DomQueryService implements DomQueryServiceInterface { rootNode, treeWalkerQueryResults, filterCallback, + ignoredTreeWalkerNodes, mutationObserver, ); @@ -233,15 +271,21 @@ export class DomQueryService implements DomQueryServiceInterface { * @param rootNode * @param treeWalkerQueryResults * @param filterCallback + * @param ignoredTreeWalkerNodes * @param mutationObserver */ private buildTreeWalkerNodesQueryResults( rootNode: Node, treeWalkerQueryResults: T[], filterCallback: CallableFunction, + ignoredTreeWalkerNodes: Set, mutationObserver?: MutationObserver, ) { - const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT); + const treeWalker = document?.createTreeWalker(rootNode, NodeFilter.SHOW_ELEMENT, (node) => + ignoredTreeWalkerNodes.has(node.nodeName?.toLowerCase()) + ? NodeFilter.FILTER_REJECT + : NodeFilter.FILTER_ACCEPT, + ); let currentNode = treeWalker?.currentNode; while (currentNode) { @@ -263,6 +307,7 @@ export class DomQueryService implements DomQueryServiceInterface { nodeShadowRoot, treeWalkerQueryResults, filterCallback, + ignoredTreeWalkerNodes, mutationObserver, ); } diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index e498384c278..019ab5e5ac4 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -12,11 +12,14 @@ import { import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { HttpStatusCode } from "@bitwarden/common/enums"; +import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -107,13 +110,24 @@ export class SsoComponent extends BaseSsoComponent implements OnInit { // show loading spinner this.loggingIn = true; try { - const response: OrganizationDomainSsoDetailsResponse = - await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email); + if (await this.configService.getFeatureFlag(FeatureFlag.VerifiedSsoDomainEndpoint)) { + const response: ListResponse = + await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(qParams.email); - if (response?.ssoAvailable && response?.verifiedDate) { - this.identifierFormControl.setValue(response.organizationIdentifier); - await this.submit(); - return; + if (response.data.length > 0) { + this.identifierFormControl.setValue(response.data[0].organizationIdentifier); + await this.submit(); + return; + } + } else { + const response: OrganizationDomainSsoDetailsResponse = + await this.orgDomainApiService.getClaimedOrgDomainByEmail(qParams.email); + + if (response?.ssoAvailable && response?.verifiedDate) { + this.identifierFormControl.setValue(response.organizationIdentifier); + await this.submit(); + return; + } } } catch (error) { this.handleGetClaimedDomainByEmailError(error); diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index 983067823cb..18b9a301510 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,7 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { generatorSwap } from "@bitwarden/angular/tools/generator/generator-swap"; import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { AnonLayoutWrapperComponent, @@ -70,6 +71,7 @@ import { RequestSMAccessComponent } from "./secrets-manager/secrets-manager-land import { SMLandingComponent } from "./secrets-manager/secrets-manager-landing/sm-landing.component"; import { DomainRulesComponent } from "./settings/domain-rules.component"; import { PreferencesComponent } from "./settings/preferences.component"; +import { CredentialGeneratorComponent } from "./tools/credential-generator/credential-generator.component"; import { GeneratorComponent } from "./tools/generator.component"; import { ReportsModule } from "./tools/reports"; import { AccessComponent } from "./tools/send/access.component"; @@ -598,11 +600,10 @@ const routes: Routes = [ titleId: "exportVault", } satisfies RouteDataProperties, }, - { + ...generatorSwap(GeneratorComponent, CredentialGeneratorComponent, { path: "generator", - component: GeneratorComponent, data: { titleId: "generator" } satisfies RouteDataProperties, - }, + }), ], }, { diff --git a/apps/web/src/app/tools/credential-generator/credential-generator.component.html b/apps/web/src/app/tools/credential-generator/credential-generator.component.html new file mode 100644 index 00000000000..901cfa65b46 --- /dev/null +++ b/apps/web/src/app/tools/credential-generator/credential-generator.component.html @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/src/app/tools/credential-generator/credential-generator.component.ts b/apps/web/src/app/tools/credential-generator/credential-generator.component.ts new file mode 100644 index 00000000000..9eb4b0a0814 --- /dev/null +++ b/apps/web/src/app/tools/credential-generator/credential-generator.component.ts @@ -0,0 +1,14 @@ +import { Component } from "@angular/core"; + +import { GeneratorModule } from "@bitwarden/generator-components"; + +import { HeaderModule } from "../../layouts/header/header.module"; +import { SharedModule } from "../../shared"; + +@Component({ + standalone: true, + selector: "credential-generator", + templateUrl: "credential-generator.component.html", + imports: [SharedModule, HeaderModule, GeneratorModule], +}) +export class CredentialGeneratorComponent {} diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 6d2196466f5..61894f29883 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1500,7 +1500,12 @@ "description": "Minimum special characters" }, "ambiguous": { - "message": "Avoid ambiguous characters" + "message": "Avoid ambiguous characters", + "description": "deprecated. Use avoidAmbiguous instead." + }, + "avoidAmbiguous": { + "message": "Avoid ambiguous characters", + "description": "Label for the avoid ambiguous characters checkbox." }, "regeneratePassword": { "message": "Regenerate password" @@ -1513,18 +1518,51 @@ }, "uppercase": { "message": "Uppercase (A-Z)", - "description": "Include uppercase letters in the password generator." + "description": "deprecated. Use uppercaseLabel instead." }, "lowercase": { "message": "Lowercase (a-z)", - "description": "Include lowercase letters in the password generator." + "description": "deprecated. Use lowercaseLabel instead." }, "numbers": { - "message": "Numbers (0-9)" + "message": "Numbers (0-9)", + "description": "deprecated. Use numbersLabel instead." }, "specialCharacters": { "message": "Special characters (!@#$%^&*)" }, + "uppercaseDescription": { + "message": "Include uppercase characters", + "description": "Tooltip for the password generator uppercase character checkbox" + }, + "uppercaseLabel": { + "message": "A-Z", + "description": "Label for the password generator uppercase character checkbox" + }, + "lowercaseDescription": { + "message": "Include lowercase characters", + "description": "Full description for the password generator lowercase character checkbox" + }, + "lowercaseLabel": { + "message": "a-z", + "description": "Label for the password generator lowercase character checkbox" + }, + "numbersDescription": { + "message": "Include numbers", + "description": "Full description for the password generator numbers checkbox" + }, + "numbersLabel": { + "message": "0-9", + "description": "Label for the password generator numbers checkbox" + }, + "specialCharactersDescription": { + "message": "Include special characters", + "description": "Full description for the password generator special characters checkbox" + }, + "specialCharactersLabel": { + "message": "!@#$%^&*", + "description": "Label for the password generator special characters checkbox" + }, "numWords": { "message": "Number of words" }, diff --git a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts index 5486250279b..d7783cfe1c9 100644 --- a/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts +++ b/libs/common/src/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction.ts @@ -1,7 +1,10 @@ +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + import { OrganizationDomainRequest } from "../../services/organization-domain/requests/organization-domain.request"; import { OrganizationDomainSsoDetailsResponse } from "./responses/organization-domain-sso-details.response"; import { OrganizationDomainResponse } from "./responses/organization-domain.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "./responses/verified-organization-domain-sso-details.response"; export abstract class OrgDomainApiServiceAbstraction { getAllByOrgId: (orgId: string) => Promise>; @@ -16,4 +19,7 @@ export abstract class OrgDomainApiServiceAbstraction { verify: (orgId: string, orgDomainId: string) => Promise; delete: (orgId: string, orgDomainId: string) => Promise; getClaimedOrgDomainByEmail: (email: string) => Promise; + getVerifiedOrgDomainsByEmail: ( + email: string, + ) => Promise>; } diff --git a/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts new file mode 100644 index 00000000000..c4817306a63 --- /dev/null +++ b/libs/common/src/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response.ts @@ -0,0 +1,15 @@ +import { BaseResponse } from "@bitwarden/common/models/response/base.response"; + +export class VerifiedOrganizationDomainSsoDetailsResponse extends BaseResponse { + organizationName: string; + organizationIdentifier: string; + domainName: string; + + constructor(response: any) { + super(response); + + this.organizationName = this.getResponseProperty("organizationName"); + this.organizationIdentifier = this.getResponseProperty("organizationIdentifier"); + this.domainName = this.getResponseProperty("domainName"); + } +} diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts index 1b9234b2fc1..7497a77e6f2 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.spec.ts @@ -1,6 +1,9 @@ import { mock } from "jest-mock-extended"; import { lastValueFrom } from "rxjs"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; +import { ListResponse } from "@bitwarden/common/models/response/list.response"; + import { ApiService } from "../../../abstractions/api.service"; import { I18nService } from "../../../platform/abstractions/i18n.service"; import { PlatformUtilsService } from "../../../platform/abstractions/platform-utils.service"; @@ -81,6 +84,19 @@ const mockedOrganizationDomainSsoDetailsResponse = new OrganizationDomainSsoDeta mockedOrganizationDomainSsoDetailsServerResponse, ); +const mockedVerifiedOrganizationDomain = { + organizationIdentifier: "fake-org-identifier", + organizationName: "fake-org", + domainName: "fake-domain-name", +}; + +const mockedVerifiedOrganizationDomainSsoResponse = + new VerifiedOrganizationDomainSsoDetailsResponse(mockedVerifiedOrganizationDomain); + +const mockedVerifiedOrganizationDomainSsoDetailsListResponse = { + data: [mockedVerifiedOrganizationDomain], +} as ListResponse; + describe("Org Domain API Service", () => { let orgDomainApiService: OrgDomainApiService; @@ -229,4 +245,21 @@ describe("Org Domain API Service", () => { expect(result).toEqual(mockedOrganizationDomainSsoDetailsResponse); }); + + it("getVerifiedOrgDomainsByEmail should call ApiService.send with correct parameters and return response", async () => { + const email = "test@example.com"; + apiService.send.mockResolvedValue(mockedVerifiedOrganizationDomainSsoDetailsListResponse); + + const result = await orgDomainApiService.getVerifiedOrgDomainsByEmail(email); + + expect(apiService.send).toHaveBeenCalledWith( + "POST", + "/organizations/domain/sso/verified", + new OrganizationDomainSsoDetailsRequest(email), + false, //anonymous + true, + ); + + expect(result.data).toContainEqual(mockedVerifiedOrganizationDomainSsoResponse); + }); }); diff --git a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts index 79b39867e2b..1424fad9b9b 100644 --- a/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts +++ b/libs/common/src/admin-console/services/organization-domain/org-domain-api.service.ts @@ -4,6 +4,7 @@ import { OrgDomainApiServiceAbstraction } from "../../abstractions/organization- import { OrgDomainInternalServiceAbstraction } from "../../abstractions/organization-domain/org-domain.service.abstraction"; import { OrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/organization-domain-sso-details.response"; import { OrganizationDomainResponse } from "../../abstractions/organization-domain/responses/organization-domain.response"; +import { VerifiedOrganizationDomainSsoDetailsResponse } from "../../abstractions/organization-domain/responses/verified-organization-domain-sso-details.response"; import { OrganizationDomainSsoDetailsRequest } from "./requests/organization-domain-sso-details.request"; import { OrganizationDomainRequest } from "./requests/organization-domain.request"; @@ -109,4 +110,18 @@ export class OrgDomainApiService implements OrgDomainApiServiceAbstraction { return response; } + + async getVerifiedOrgDomainsByEmail( + email: string, + ): Promise> { + const result = await this.apiService.send( + "POST", + `/organizations/domain/sso/verified`, + new OrganizationDomainSsoDetailsRequest(email), + false, // anonymous + true, + ); + + return new ListResponse(result, VerifiedOrganizationDomainSsoDetailsResponse); + } } diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 676acb61575..45b02471f3c 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -31,6 +31,7 @@ export enum FeatureFlag { NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements", AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api", CipherKeyEncryption = "cipher-key-encryption", + VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint", PM11901_RefactorSelfHostingLicenseUploader = "PM-11901-refactor-self-hosting-license-uploader", Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api", AccessIntelligence = "pm-13227-access-intelligence", @@ -75,6 +76,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.NotificationBarAddLoginImprovements]: FALSE, [FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE, [FeatureFlag.CipherKeyEncryption]: FALSE, + [FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE, [FeatureFlag.PM11901_RefactorSelfHostingLicenseUploader]: FALSE, [FeatureFlag.Pm3478RefactorOrganizationUserApi]: FALSE, [FeatureFlag.AccessIntelligence]: FALSE, diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index b8458fa9c12..9699f832ed0 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -12,7 +12,8 @@ > - {{ "password" | i18n }} + {{ "password" | i18n }} + {{ "newPassword" | i18n }}