1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-09 21:20:27 +00:00

Merge branch 'main' into auth/pm-9115/implement-view-data-persistence-in-2FA-flows

This commit is contained in:
Alec Rippberger
2025-04-02 08:58:07 -05:00
committed by GitHub
38 changed files with 354 additions and 2057 deletions

View File

@@ -42,10 +42,11 @@ on:
env:
_AZ_REGISTRY: bitwardenprod.azurecr.io
jobs:
setup:
name: Setup
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.version.outputs.value }}
node_version: ${{ steps.retrieve-node-version.outputs.node_version }}
@@ -54,7 +55,7 @@ jobs:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Get GitHub sha as version
id: version
@@ -75,133 +76,61 @@ jobs:
has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
build-artifacts:
name: Build artifacts
runs-on: ubuntu-22.04
needs:
- setup
env:
_VERSION: ${{ needs.setup.outputs.version }}
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
strategy:
matrix:
include:
- name: "selfhosted-open-source"
npm_command: "dist:oss:selfhost"
- name: "cloud-COMMERCIAL"
npm_command: "dist:bit:cloud"
- name: "selfhosted-COMMERCIAL"
npm_command: "dist:bit:selfhost"
- name: "cloud-QA"
npm_command: "build:bit:qa"
git_metadata: true
- name: "ee"
npm_command: "build:bit:ee"
git_metadata: true
- name: "cloud-euprd"
npm_command: "build:bit:euprd"
- name: "cloud-euqa"
npm_command: "build:bit:euqa"
git_metadata: true
- name: "cloud-usdev"
npm_command: "build:bit:usdev"
git_metadata: true
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
node-version: ${{ env._NODE_VERSION }}
- name: Print environment
run: |
whoami
node --version
npm --version
docker --version
echo "GitHub ref: $GITHUB_REF"
echo "GitHub event: $GITHUB_EVENT"
- name: Install dependencies
run: npm ci
- name: Download SDK Artifacts
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
uses: bitwarden/gh-actions/download-artifacts@main
with:
github_token: ${{secrets.GITHUB_TOKEN}}
workflow: build-wasm-internal.yml
workflow_conclusion: success
branch: ${{ inputs.sdk_branch }}
artifacts: sdk-internal
repo: bitwarden/sdk-internal
path: ../sdk-internal
if_no_artifact_found: fail
- name: Override SDK
if: ${{ inputs.sdk_branch != '' && needs.setup.outputs.has_secrets == 'true' }}
working-directory: ./
run: |
ls -l ../
npm link ../sdk-internal
- name: Add Git metadata to build version
working-directory: apps/web
if: matrix.git_metadata
run: |
VERSION=$( jq -r ".version" package.json)
jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
- name: Build ${{ matrix.name }}
working-directory: apps/web
run: npm run ${{ matrix.npm_command }}
- name: Package artifact
working-directory: apps/web
run: zip -r web-${{ env._VERSION }}-${{ matrix.name }}.zip build
- name: Upload ${{ matrix.name }} artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: web-${{ env._VERSION }}-${{ matrix.name }}.zip
path: apps/web/web-${{ env._VERSION }}-${{ matrix.name }}.zip
if-no-files-found: error
build-containers:
name: Build Docker images
runs-on: ubuntu-22.04
name: Build artifacts and container images
runs-on: ubuntu-24.04
permissions:
security-events: write
id-token: write
needs:
- setup
- build-artifacts
needs: setup
strategy:
fail-fast: false
matrix:
include:
- artifact_name: cloud-QA
image_name: web-qa-cloud
- artifact_name: ee
image_name: web-ee
- artifact_name: selfhosted-open-source
image_name: web-oss
npm_command: dist:oss:selfhost
- artifact_name: cloud-COMMERCIAL
image_name: web-cloud
npm_command: dist:bit:cloud
- artifact_name: selfhosted-COMMERCIAL
image_name: web
npm_command: dist:bit:selfhost
- artifact_name: cloud-QA
image_name: web-qa-cloud
npm_command: build:bit:qa
git_metadata: true
- artifact_name: ee
image_name: web-ee
npm_command: build:bit:ee
git_metadata: true
- artifact_name: cloud-euprd
image_name: web-euprd
npm_command: build:bit:euprd
- artifact_name: cloud-euqa
image_name: web-euqa
npm_command: build:bit:euqa
git_metadata: true
- artifact_name: cloud-usdev
image_name: web-usdev
npm_command: build:bit:usdev
git_metadata: true
env:
_NODE_VERSION: ${{ needs.setup.outputs.node_version }}
_VERSION: ${{ needs.setup.outputs.version }}
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Check out Server repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
path: server
repository: bitwarden/server
ref: ${{ github.event.pull_request.head.sha && 'main' || github.ref }}
- name: Check Branch to Publish
env:
@@ -216,6 +145,21 @@ jobs:
echo "is_publish_branch=false" >> $GITHUB_ENV
fi
- name: Add Git metadata to build version
working-directory: apps/web
if: matrix.git_metadata
run: |
VERSION=$( jq -r ".version" package.json)
jq --arg version "$VERSION+${GITHUB_SHA:0:7}" '.version = $version' package.json > package.json.tmp
mv package.json.tmp package.json
########## Set up Docker ##########
- name: Set up QEMU emulators
uses: docker/setup-qemu-action@68827325e0b33c7199eb31dd4e31fbe9023e06e3 # v3.0.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
########## ACRs ##########
- name: Login to Prod Azure
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
@@ -225,7 +169,7 @@ jobs:
- name: Log into Prod container registry
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
run: az acr login -n bitwardenprod
run: az acr login -n ${_AZ_REGISTRY%.azurecr.io}
- name: Login to Azure - CI Subscription
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
@@ -241,14 +185,8 @@ jobs:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Download ${{ matrix.artifact_name }} artifact
uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8
with:
name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
path: apps/web
########## Generate image tag and build Docker image ##########
- name: Generate Docker image tag
- name: Generate container image tag
id: tag
run: |
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
@@ -270,10 +208,6 @@ jobs:
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
########## Build Image ##########
- name: Extract artifact
working-directory: apps/web
run: unzip web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
- name: Generate image full name
id: image-name
env:
@@ -283,16 +217,38 @@ jobs:
- name: Build Docker image
if: ${{ needs.setup.outputs.has_secrets == 'true' }}
id: build-docker
id: build-container
uses: docker/build-push-action@67a2d409c0a876cbe6b11854e3e25193efe4e62d # v6.12.0
with:
context: apps/web
build-args: |
NODE_VERSION=${{ env._NODE_VERSION }}
NPM_COMMAND=${{ matrix.npm_command }}
context: .
file: apps/web/Dockerfile
platforms: linux/amd64
platforms: |
linux/amd64,
linux/arm/v7,
linux/arm64
push: true
tags: ${{ steps.image-name.outputs.name }}
secrets: |
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
- name: Zip project
working-directory: apps/web
env:
IMAGE_NAME: ${{ steps.image-name.outputs.name }}
run: |
mkdir build
docker run --rm --volume $(pwd)/build:/temp --entrypoint bash \
$IMAGE_NAME -c "cp -r ./ /temp"
zip -r web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip build
- name: Upload ${{ matrix.artifact_name }} artifact
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
with:
name: web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
path: apps/web/web-${{ env._VERSION }}-${{ matrix.artifact_name }}.zip
if-no-files-found: error
- name: Install Cosign
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
@@ -301,7 +257,7 @@ jobs:
- name: Sign image with Cosign
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
env:
DIGEST: ${{ steps.build-docker.outputs.digest }}
DIGEST: ${{ steps.build-container.outputs.digest }}
TAGS: ${{ steps.image-name.outputs.name }}
run: |
IFS="," read -a tags <<< "${TAGS}"
@@ -329,19 +285,19 @@ jobs:
ref: ${{ contains(github.event_name, 'pull_request') && format('refs/pull/{0}/head', github.event.pull_request.number) || github.ref }}
- name: Log out of Docker
run: docker logout
run: docker logout $_AZ_REGISTRY
crowdin-push:
name: Crowdin Push
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
needs:
- build-artifacts
runs-on: ubuntu-22.04
needs: build-containers
runs-on: ubuntu-24.04
steps:
- name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
ref: ${{ github.event.pull_request.head.sha }}
ref: ${{ github.event.pull_request.head.sha }}
- name: Login to Azure
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -367,12 +323,12 @@ jobs:
upload_sources: true
upload_translations: false
trigger-web-vault-deploy:
name: Trigger web vault deploy
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04
needs:
- build-artifacts
runs-on: ubuntu-24.04
needs: build-containers
steps:
- name: Login to Azure - CI Subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@@ -402,13 +358,13 @@ jobs:
}
})
check-failures:
name: Check for failures
if: always()
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
needs:
- setup
- build-artifacts
- build-containers
- crowdin-push
- trigger-web-vault-deploy

View File

@@ -1,105 +0,0 @@
import { Component, Inject } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
import { BrowserApi } from "../../platform/browser/browser-api";
@Component({
selector: "app-sso",
templateUrl: "sso-v1.component.html",
})
export class SsoComponentV1 extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
syncService: SyncService,
environmentService: EnvironmentService,
logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
private authService: AuthService,
@Inject(WINDOW) private win: Window,
toastService: ToastService,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService,
userDecryptionOptionsService,
configService,
masterPasswordService,
accountService,
toastService,
);
environmentService.environment$.pipe(takeUntilDestroyed()).subscribe((env) => {
this.redirectUri = env.getWebVaultUrl() + "/sso-connector.html";
});
this.clientId = "browser";
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
// If the vault is unlocked then this will clear keys from memory, which we don't want to do
if ((await this.authService.getAuthStatus()) !== AuthenticationStatus.Unlocked) {
BrowserApi.reloadOpenWindows();
}
this.win.close();
};
this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
this.onSuccessfulLoginTdeNavigate = async () => {
this.win.close();
};
}
}

View File

@@ -1,8 +1,6 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject } from "rxjs";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { ServerConfig } from "@bitwarden/common/platform/abstractions/config/server-config";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentServerConfigData } from "@bitwarden/common/platform/models/data/server-config.data";
@@ -25,8 +23,6 @@ import { OverlayNotificationsBackground } from "./overlay-notifications.backgrou
describe("OverlayNotificationsBackground", () => {
let logService: MockProxy<LogService>;
let getFeatureFlagMock$: BehaviorSubject<boolean>;
let configService: MockProxy<ConfigService>;
let notificationBackground: NotificationBackground;
let getEnableChangedPasswordPromptSpy: jest.SpyInstance;
let getEnableAddedLoginPromptSpy: jest.SpyInstance;
@@ -35,10 +31,6 @@ describe("OverlayNotificationsBackground", () => {
beforeEach(async () => {
jest.useFakeTimers();
logService = mock<LogService>();
getFeatureFlagMock$ = new BehaviorSubject(true);
configService = mock<ConfigService>({
getFeatureFlag$: jest.fn().mockReturnValue(getFeatureFlagMock$),
});
notificationBackground = mock<NotificationBackground>();
getEnableChangedPasswordPromptSpy = jest
.spyOn(notificationBackground, "getEnableChangedPasswordPrompt")
@@ -48,10 +40,8 @@ describe("OverlayNotificationsBackground", () => {
.mockResolvedValue(true);
overlayNotificationsBackground = new OverlayNotificationsBackground(
logService,
configService,
notificationBackground,
);
configService.getFeatureFlag.mockResolvedValue(true);
await overlayNotificationsBackground.init();
});
@@ -60,27 +50,6 @@ describe("OverlayNotificationsBackground", () => {
jest.clearAllTimers();
});
describe("feature flag behavior", () => {
let runtimeRemoveListenerSpy: jest.SpyInstance;
beforeEach(() => {
runtimeRemoveListenerSpy = jest.spyOn(chrome.runtime.onMessage, "removeListener");
});
it("removes the extension listeners if the current flag value is set to `false`", () => {
getFeatureFlagMock$.next(false);
expect(runtimeRemoveListenerSpy).toHaveBeenCalled();
});
it("ignores the feature flag change if the previous flag value is equal to the current flag value", () => {
getFeatureFlagMock$.next(false);
getFeatureFlagMock$.next(false);
expect(runtimeRemoveListenerSpy).toHaveBeenCalledTimes(1);
});
});
describe("setting up the form submission listeners", () => {
let fields: MockProxy<AutofillField>[];
let details: MockProxy<AutofillPageDetails>;

View File

@@ -1,11 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { startWith, Subject, Subscription, switchMap, timer } from "rxjs";
import { pairwise } from "rxjs/operators";
import { Subject, switchMap, timer } from "rxjs";
import { CLEAR_NOTIFICATION_LOGIN_DATA_DURATION } from "@bitwarden/common/autofill/constants";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { BrowserApi } from "../../platform/browser/browser-api";
@@ -26,7 +23,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
private websiteOriginsWithFields: WebsiteOriginsWithFields = new Map();
private activeFormSubmissionRequests: ActiveFormSubmissionRequests = new Set();
private modifyLoginCipherFormData: ModifyLoginCipherFormDataForTab = new Map();
private featureFlagState$: Subscription;
private clearLoginCipherFormDataSubject: Subject<void> = new Subject();
private notificationFallbackTimeout: number | NodeJS.Timeout | null;
private readonly formSubmissionRequestMethods: Set<string> = new Set(["POST", "PUT", "PATCH"]);
@@ -38,7 +34,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
constructor(
private logService: LogService,
private configService: ConfigService,
private notificationBackground: NotificationBackground,
) {}
@@ -46,35 +41,13 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
* Initialize the overlay notifications background service.
*/
async init() {
this.featureFlagState$ = this.configService
.getFeatureFlag$(FeatureFlag.NotificationBarAddLoginImprovements)
.pipe(startWith(undefined), pairwise())
.subscribe(([prev, current]) => this.handleInitFeatureFlagChange(prev, current));
this.setupExtensionListeners();
this.clearLoginCipherFormDataSubject
.pipe(switchMap(() => timer(CLEAR_NOTIFICATION_LOGIN_DATA_DURATION)))
.subscribe(() => this.modifyLoginCipherFormData.clear());
}
/**
* Handles enabling/disabling the extension listeners that trigger the
* overlay notifications based on the feature flag state.
*
* @param previousValue - The previous value of the feature flag
* @param currentValue - The current value of the feature flag
*/
private handleInitFeatureFlagChange = (previousValue: boolean, currentValue: boolean) => {
if (previousValue === currentValue) {
return;
}
if (currentValue) {
this.setupExtensionListeners();
return;
}
this.removeExtensionListeners();
};
/**
* Handles the response from the content script with the page details. Triggers an initialization
* of the add login or change password notification if the conditions are met.
@@ -520,15 +493,6 @@ export class OverlayNotificationsBackground implements OverlayNotificationsBackg
chrome.tabs.onUpdated.addListener(this.handleTabUpdated);
}
/**
* Removes the listeners for the extension messages and the tab events.
*/
private removeExtensionListeners() {
BrowserApi.removeListener(chrome.runtime.onMessage, this.handleExtensionMessage);
chrome.tabs.onRemoved.removeListener(this.handleTabRemoved);
chrome.tabs.onUpdated.removeListener(this.handleTabUpdated);
}
/**
* Handles messages that are sent to the extension background.
*

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,6 @@ const NotificationTypes = {
type NotificationType = (typeof NotificationTypes)[keyof typeof NotificationTypes];
type NotificationBarIframeInitData = {
applyRedesign?: boolean;
ciphers?: NotificationCipherData[];
folders?: FolderView[];
importType?: string;

View File

@@ -7,7 +7,7 @@
<body>
<div id="notification-bar-outer-wrapper" class="outer-wrapper">
<div class="logo">
<div class="logo-wrapper">
<a href="https://vault.bitwarden.com" target="_blank" id="logo-link" rel="noreferrer">
<img id="logo" alt="Bitwarden" />
</a>

View File

@@ -1,8 +1,8 @@
@import "../shared/styles/variables";
body {
padding: 0;
margin: 0;
padding: 0;
height: 100%;
font-size: 14px;
line-height: 16px;
@@ -14,16 +14,30 @@ body {
}
}
img {
margin: 0;
padding: 0;
border: 0;
}
button,
select {
font-size: $font-size-base;
font-family: $font-family-sans-serif;
}
.outer-wrapper {
padding: 0 10px;
border-bottom: 2px solid transparent;
display: grid;
grid-template-columns: 24px auto 55px;
grid-column-gap: 10px;
box-sizing: border-box;
display: block;
position: relative;
padding: 8px;
min-height: 42px;
border: 1px solid transparent;
border-bottom: 2px solid transparent;
border-radius: 4px;
box-sizing: border-box;
@include themify($themes) {
border-color: themed("borderColor");
border-bottom-color: themed("primaryColor");
}
@@ -50,20 +64,34 @@ body {
align-self: center;
}
img {
border: 0;
margin: 0;
padding: 0;
}
#logo {
width: 24px;
height: 24px;
display: block;
}
.logo-wrapper {
position: absolute;
top: 8px;
left: 10px;
overflow: hidden;
}
#close-button {
display: flex;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
margin-right: 10px;
padding: 0;
&:hover {
@include themify($themes) {
border-color: rgba(themed("textColor"), 0.2);
background-color: rgba(themed("textColor"), 0.2);
}
}
}
#close {
@@ -78,15 +106,79 @@ img {
}
}
#close-button:hover {
@include themify($themes) {
border-color: rgba(themed("textColor"), 0.2);
background-color: rgba(themed("textColor"), 0.2);
.notification-close {
position: absolute;
top: 6px;
right: 6px;
}
#content .inner-wrapper {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
.notification-body {
width: 100%;
padding: 4px 38px 24px 42px;
font-weight: 400;
}
.notification-actions {
display: flex;
width: 100%;
align-items: stretch;
justify-content: flex-end;
#never-save {
margin-right: auto;
padding: 0;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.5px;
}
#select-folder {
width: 125px;
margin-right: 6px;
font-size: 12px;
appearance: none;
background-repeat: no-repeat;
background-position: center right 4px;
background-size: 16px;
@include themify($themes) {
color: themed("mutedTextColor");
border-color: themed("mutedTextColor");
}
&:not([disabled]) {
display: block;
}
}
.primary,
.secondary {
font-size: 12px;
}
.secondary {
margin-right: 6px;
border-width: 1px;
}
.primary {
margin-right: 2px;
}
&.success-message,
&.error-message {
padding: 4px 36px 6px 42px;
}
}
}
button {
padding: 0.35rem 15px;
padding: 4px 8px;
border-radius: $border-radius;
border: 1px solid transparent;
cursor: pointer;
@@ -133,14 +225,13 @@ button.neutral {
text-decoration: underline;
@include themify($themes) {
background-color: transparent;
color: darken(themed("primaryColor"), 6%);
}
}
}
select {
padding: 0.35rem;
padding: 4px 6px;
border: 1px solid #000000;
border-radius: $border-radius;
@@ -151,16 +242,9 @@ select {
}
}
select,
button {
font-size: $font-size-base;
font-family: $font-family-sans-serif;
}
.success-message {
display: flex;
align-items: center;
align-content: center;
justify-content: center;
@include themify($themes) {
@@ -184,6 +268,13 @@ button {
}
}
.success-event,
.error-event {
.notification-body {
display: none;
}
}
@media screen and (max-width: 768px) {
#select-folder {
display: none;
@@ -196,131 +287,8 @@ button {
}
}
.notification-bar-redesign {
button {
padding: 4px 8px;
}
select {
padding: 4px 6px;
}
.outer-wrapper {
display: block;
position: relative;
padding: 8px;
border-top: 1px solid transparent;
border-left: 1px solid transparent;
border-right: 1px solid transparent;
border-radius: 4px;
@include themify($themes) {
border-top-color: themed("borderColor");
border-left-color: themed("borderColor");
border-right-color: themed("borderColor");
}
}
.logo {
position: absolute;
top: 8px;
left: 10px;
overflow: hidden;
}
.notification-close {
position: absolute;
top: 6px;
right: 6px;
#close-button {
display: flex;
align-content: center;
align-items: center;
justify-content: center;
width: 30px;
height: 30px;
padding: 0;
margin: 0;
}
}
#content .inner-wrapper {
display: flex;
align-items: flex-start;
align-content: flex-start;
flex-wrap: wrap;
.notification-body {
width: 100%;
padding: 4px 38px 24px 42px;
font-weight: 400;
}
.notification-actions {
display: flex;
align-items: stretch;
align-content: stretch;
justify-content: flex-end;
width: 100%;
#never-save {
margin-right: auto;
padding: 0;
font-size: 16px;
font-weight: 500;
letter-spacing: 0.5px;
}
#select-folder {
width: 125px;
margin-right: 6px;
font-size: 12px;
appearance: none;
background-size: 16px;
background-position: center right 4px;
background-repeat: no-repeat;
@include themify($themes) {
color: themed("mutedTextColor");
border-color: themed("mutedTextColor");
}
&:not([disabled]) {
display: block;
}
}
.primary,
.secondary {
font-size: 12px;
}
.secondary {
margin-right: 6px;
border-width: 1px;
}
.primary {
margin-right: 2px;
}
&.success-message,
&.error-message {
padding: 4px 36px 6px 42px;
}
}
}
.success-event,
.error-event {
.notification-body {
display: none;
}
}
}
.theme_light {
.notification-bar-redesign #content .inner-wrapper {
#content .inner-wrapper {
#select-folder {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHhtbG5zOnhsaW5rPSdodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rJyB3aWR0aD0nMTYnIGhlaWdodD0nMTYnIGZpbGw9J25vbmUnPjxwYXRoIHN0cm9rZT0nIzIxMjUyOScgZD0nbTUgNiAzIDMgMy0zJy8+PC9zdmc+");
}
@@ -328,7 +296,7 @@ button {
}
.theme_dark {
.notification-bar-redesign #content .inner-wrapper {
#content .inner-wrapper {
#select-folder {
background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPScxNicgaGVpZ2h0PScxNicgZmlsbD0nbm9uZSc+PHBhdGggc3Ryb2tlPScjZmZmZmZmJyBkPSdtNSA2IDMgMyAzLTMnLz48L3N2Zz4=");
}

View File

@@ -507,10 +507,6 @@ function setNotificationBarTheme() {
const theme = getTheme(globalThis, notificationBarIframeInitData.theme);
document.documentElement.classList.add(`theme_${theme}`);
if (notificationBarIframeInitData.applyRedesign) {
document.body.classList.add("notification-bar-redesign");
}
}
function postMessageToParent(message: NotificationBarWindowMessage) {

View File

@@ -17,6 +17,7 @@ export class OverlayNotificationsContentService
private notificationBarIframeElement: HTMLIFrameElement | null = null;
private currentNotificationBarType: string | null = null;
private removeTabFromNotificationQueueTypes = new Set(["add", "change"]);
private notificationRefreshFlag: boolean;
private notificationBarElementStyles: Partial<CSSStyleDeclaration> = {
height: "82px",
width: "430px",
@@ -54,6 +55,9 @@ export class OverlayNotificationsContentService
constructor() {
void sendExtensionMessage("checkNotificationQueue");
void sendExtensionMessage("notificationRefreshFlagValue").then((notificationRefreshFlag) => {
this.notificationRefreshFlag = !!notificationRefreshFlag;
});
}
/**
@@ -84,7 +88,6 @@ export class OverlayNotificationsContentService
theme: typeData.theme,
removeIndividualVault: typeData.removeIndividualVault,
importType: typeData.importType,
applyRedesign: true,
launchTimestamp: typeData.launchTimestamp,
};
@@ -192,7 +195,13 @@ export class OverlayNotificationsContentService
{ transform: "translateX(0)", opacity: "1" },
true,
);
setElementStyles(this.notificationBarElement, { boxShadow: "2px 4px 6px 0px #0000001A" }, true);
if (!this.notificationRefreshFlag) {
setElementStyles(
this.notificationBarElement,
{ boxShadow: "2px 4px 6px 0px #0000001A" },
true,
);
}
this.notificationBarIframeElement.removeEventListener(
EVENTS.LOAD,
this.handleNotificationBarIframeOnLoad,
@@ -206,7 +215,13 @@ export class OverlayNotificationsContentService
if (this.notificationBarIframeElement) {
this.notificationBarElement = globalThis.document.createElement("div");
this.notificationBarElement.id = "bit-notification-bar";
setElementStyles(this.notificationBarElement, this.notificationBarElementStyles, true);
if (this.notificationRefreshFlag) {
setElementStyles(this.notificationBarElement, { height: "400px", right: "0" }, true);
}
this.notificationBarElement.appendChild(this.notificationBarIframeElement);
}
}

View File

@@ -380,7 +380,7 @@ describe("AutofillService", () => {
const autofillOverlayMenuBootstrapScript = "bootstrap-autofill-overlay-menu.js";
const autofillOverlayNotificationsBootstrapScript =
"bootstrap-autofill-overlay-notifications.js";
const defaultAutofillScripts = ["autofiller.js", "notificationBar.js", "contextMenuHandler.js"];
const defaultAutofillScripts = ["autofiller.js", "contextMenuHandler.js"];
const defaultExecuteScriptOptions = { runAt: "document_start" };
let tabMock: chrome.tabs.Tab;
let sender: chrome.runtime.MessageSender;
@@ -400,13 +400,9 @@ describe("AutofillService", () => {
});
it("accepts an extension message sender and injects the autofill scripts into the tab of the sender", async () => {
configService.getFeatureFlag.mockImplementation(async (_feature) => {
if (_feature === FeatureFlag.NotificationBarAddLoginImprovements) {
return false as FeatureFlagValueType<any>;
}
enableChangedPasswordPromptMock$.next(false);
enableAddedLoginPromptMock$.next(false);
return true as FeatureFlagValueType<any>;
});
await autofillService.injectAutofillScripts(sender.tab, sender.frameId, true);
[autofillOverlayMenuBootstrapScript, ...defaultAutofillScripts].forEach((scriptName) => {
@@ -457,25 +453,12 @@ describe("AutofillService", () => {
});
});
it("will inject the bootstrap-autofill script if the user does not have the autofill overlay enabled", async () => {
it("will inject the overlay script if the user does not have the autofill overlay enabled", async () => {
jest
.spyOn(autofillService, "getInlineMenuVisibility")
.mockResolvedValue(AutofillOverlayVisibility.Off);
configService.getFeatureFlag.mockImplementation(async (_feature) => {
if (_feature === FeatureFlag.NotificationBarAddLoginImprovements) {
return false as FeatureFlagValueType<any>;
}
return true as FeatureFlagValueType<any>;
});
await autofillService.injectAutofillScripts(sender.tab, sender.frameId);
expect(BrowserApi.executeScriptInTab).toHaveBeenCalledWith(tabMock.id, {
file: `content/${autofillBootstrapScript}`,
frameId: sender.frameId,
...defaultExecuteScriptOptions,
});
expect(BrowserApi.executeScriptInTab).not.toHaveBeenCalledWith(tabMock.id, {
file: `content/${autofillOverlayBootstrapScript}`,
frameId: sender.frameId,

View File

@@ -236,13 +236,8 @@ export default class AutofillService implements AutofillServiceInterface {
const authStatus = await firstValueFrom(this.authService.activeAccountStatus$);
const accountIsUnlocked = authStatus === AuthenticationStatus.Unlocked;
let autoFillOnPageLoadIsEnabled = false;
const addLoginImprovementsFlagActive = await this.configService.getFeatureFlag(
FeatureFlag.NotificationBarAddLoginImprovements,
);
const injectedScripts = [
await this.getBootstrapAutofillContentScript(activeAccount, addLoginImprovementsFlagActive),
];
const injectedScripts = [await this.getBootstrapAutofillContentScript(activeAccount)];
if (activeAccount && accountIsUnlocked) {
autoFillOnPageLoadIsEnabled = await this.getAutofillOnPageLoad();
@@ -259,10 +254,6 @@ export default class AutofillService implements AutofillServiceInterface {
});
}
if (!addLoginImprovementsFlagActive) {
injectedScripts.push("notificationBar.js");
}
injectedScripts.push("contextMenuHandler.js");
for (const injectedScript of injectedScripts) {
@@ -283,11 +274,9 @@ export default class AutofillService implements AutofillServiceInterface {
* enabled.
*
* @param activeAccount - The active account
* @param addLoginImprovementsFlagActive - Whether the add login improvements feature flag is active
*/
private async getBootstrapAutofillContentScript(
activeAccount: { id: UserId | undefined } & AccountInfo,
addLoginImprovementsFlagActive = false,
): Promise<string> {
let inlineMenuVisibility: InlineMenuVisibilitySetting = AutofillOverlayVisibility.Off;
@@ -310,8 +299,7 @@ export default class AutofillService implements AutofillServiceInterface {
const enableAddedLoginPrompt = await firstValueFrom(
this.userNotificationSettingsService.enableAddedLoginPrompt$,
);
const isNotificationBarEnabled =
addLoginImprovementsFlagActive && (enableChangedPasswordPrompt || enableAddedLoginPrompt);
const isNotificationBarEnabled = enableChangedPasswordPrompt || enableAddedLoginPrompt;
if (!inlineMenuVisibility && !isNotificationBarEnabled) {
return "bootstrap-autofill.js";

View File

@@ -1195,7 +1195,6 @@ export default class MainBackground {
this.overlayNotificationsBackground = new OverlayNotificationsBackground(
this.logService,
this.configService,
this.notificationBackground,
);

View File

@@ -24,7 +24,6 @@ import { RemovePasswordComponent } from "../auth/popup/remove-password.component
import { SetPasswordComponent } from "../auth/popup/set-password.component";
import { AccountSecurityComponent } from "../auth/popup/settings/account-security.component";
import { VaultTimeoutInputComponent } from "../auth/popup/settings/vault-timeout-input.component";
import { SsoComponentV1 } from "../auth/popup/sso-v1.component";
import { UpdateTempPasswordComponent } from "../auth/popup/update-temp-password.component";
import { AutofillComponent } from "../autofill/popup/settings/autofill.component";
import { NotificationsSettingsComponent } from "../autofill/popup/settings/notifications.component";
@@ -89,7 +88,6 @@ import "../platform/popup/locales";
ColorPasswordPipe,
ColorPasswordCountPipe,
SetPasswordComponent,
SsoComponentV1,
TabsV2Component,
UpdateTempPasswordComponent,
UserVerificationComponent,

View File

@@ -201,7 +201,6 @@ const mainConfig = {
"./src/autofill/deprecated/content/bootstrap-legacy-autofill-overlay.ts",
"content/autofiller": "./src/autofill/content/autofiller.ts",
"content/auto-submit-login": "./src/autofill/content/auto-submit-login.ts",
"content/notificationBar": "./src/autofill/content/notification-bar.ts",
"content/contextMenuHandler": "./src/autofill/content/context-menu-handler.ts",
"content/content-message-handler": "./src/autofill/content/content-message-handler.ts",
"content/fido2-content-script": "./src/autofill/fido2/content/fido2-content-script.ts",

View File

@@ -15,7 +15,6 @@ import { DeleteAccountComponent } from "../auth/delete-account.component";
import { LoginModule } from "../auth/login/login.module";
import { RemovePasswordComponent } from "../auth/remove-password.component";
import { SetPasswordComponent } from "../auth/set-password.component";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { SshAgentService } from "../autofill/services/ssh-agent.service";
import { PremiumComponent } from "../billing/app/accounts/premium.component";
@@ -76,7 +75,6 @@ import { SharedModule } from "./shared/shared.module";
SetPasswordComponent,
SettingsComponent,
ShareComponent,
SsoComponentV1,
UpdateTempPasswordComponent,
VaultComponent,
VaultTimeoutInputComponent,

View File

@@ -1,9 +0,0 @@
<form id="sso-page" (ngSubmit)="submit()">
<div class="content">
<img class="logo-image" alt="Bitwarden" />
<div class="box">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
</div>
</form>

View File

@@ -1,83 +0,0 @@
import { Component } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction";
import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@Component({
selector: "app-sso",
templateUrl: "sso-v1.component.html",
})
export class SsoComponentV1 extends BaseSsoComponent {
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
syncService: SyncService,
route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
toastService: ToastService,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService,
userDecryptionOptionsService,
configService,
masterPasswordService,
accountService,
toastService,
);
this.onSuccessfulLogin = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
this.onSuccessfulLoginTde = async () => {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
syncService.fullSync(true);
};
this.redirectUri = "bitwarden://sso-callback";
this.clientId = "desktop";
}
}

View File

@@ -1,7 +1,6 @@
@import "variables.scss";
#lock-page,
#sso-page,
#set-password-page,
#remove-password-page {
display: flex;
@@ -106,18 +105,6 @@
}
}
#sso-page {
.content {
width: 325px;
.box {
margin-top: 30px;
margin-bottom: 30px;
text-align: center;
}
}
}
#set-password-page,
#remove-password-page {
.content {

View File

@@ -1,18 +1,83 @@
FROM ghcr.io/bitwarden/server
###############################################
# Build stage 1 #
###############################################
ARG NODE_VERSION=20
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS node-build
ARG NPM_COMMAND=dist:bit:selfhost
WORKDIR /source
COPY . .
RUN npm ci
WORKDIR /source/apps/web
RUN npm run ${NPM_COMMAND}
###############################################
# Build stage 2 #
###############################################
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 AS build
# Docker buildx supplies the value for this arg
ARG TARGETPLATFORM
# Determine proper runtime value for .NET
# We put the value in a file to be read by later layers.
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
RID=linux-x64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
RID=linux-arm64 ; \
elif [ "$TARGETPLATFORM" = "linux/arm/v7" ]; then \
RID=linux-arm ; \
fi \
&& echo "RID=$RID" > /tmp/rid.txt
# Copy csproj files as distinct layers
WORKDIR /source
COPY server/util/Server/*.csproj ./util/Server/
COPY server/Directory.Build.props .
COPY server/.editorconfig .
# Restore Server project dependencies and tools
WORKDIR /source/util/Server
RUN . /tmp/rid.txt && dotnet restore -r $RID
# Copy required project files
WORKDIR /source
COPY server/util/Server/. ./util/Server/
COPY server/.git/. ./.git/
# Build Server app
WORKDIR /source/util/Server
RUN . /tmp/rid.txt && dotnet publish -c release -o /app/Server --no-restore --no-self-contained -r $RID
WORKDIR /app
###############################################
# App stage #
###############################################
FROM mcr.microsoft.com/dotnet/aspnet:8.0
ARG TARGETPLATFORM
LABEL com.bitwarden.product="bitwarden"
ENV ASPNETCORE_ENVIRONMENT=Production
ENV ASPNETCORE_URLS=http://+:5000
EXPOSE 5000
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
gosu \
curl \
&& rm -rf /var/lib/apt/lists/*
&& rm -rf /var/lib/apt/lists/*
# Copy app from the build stage
WORKDIR /bitwarden_server
COPY --from=build /app/Server ./
ENV ASPNETCORE_URLS http://+:5000
WORKDIR /app
EXPOSE 5000
COPY ./build .
COPY entrypoint.sh /
COPY --from=node-build /source/apps/web/build .
COPY --from=node-build /source/apps/web/entrypoint.sh /
RUN chmod +x /entrypoint.sh
HEALTHCHECK CMD curl -f http://localhost:5000 || exit 1

View File

@@ -111,10 +111,10 @@
<ng-container *ngIf="loaded && usePlaceHolderEvents">
<div
class="tw-relative tw--top-72 tw-bg-[#ffffff] tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center tw-h-[19rem]"
class="tw-relative tw--top-72 tw-bg-background tw-bg-opacity-90 tw-pb-5 tw-flex tw-items-center tw-justify-center tw-h-[19rem]"
>
<div
class="tw-bg-[#ffffff] tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
class="tw-bg-background tw-max-w-xl tw-flex-col tw-justify-center tw-text-center tw-p-5 tw-px-10 tw-rounded tw-border-0 tw-border-b tw-border-secondary-300 tw-border-solid tw-mt-5"
>
<i class="bwi bwi-2x bwi-business tw-text-primary-600"></i>

View File

@@ -1,66 +0,0 @@
import { Component } from "@angular/core";
import { AttachmentsComponent as BaseAttachmentsComponent } from "@bitwarden/angular/vault/components/attachments.component";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { DialogService, ToastService } from "@bitwarden/components";
import { KeyService } from "@bitwarden/key-management";
@Component({
selector: "emergency-access-attachments",
templateUrl: "../../../../vault/individual-vault/attachments.component.html",
})
export class EmergencyAccessAttachmentsComponent extends BaseAttachmentsComponent {
viewOnly = true;
canAccessAttachments = true;
constructor(
cipherService: CipherService,
i18nService: I18nService,
keyService: KeyService,
encryptService: EncryptService,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
logService: LogService,
fileDownloadService: FileDownloadService,
dialogService: DialogService,
billingAccountProfileStateService: BillingAccountProfileStateService,
accountService: AccountService,
toastService: ToastService,
) {
super(
cipherService,
i18nService,
keyService,
encryptService,
platformUtilsService,
apiService,
window,
logService,
stateService,
fileDownloadService,
dialogService,
billingAccountProfileStateService,
accountService,
toastService,
);
}
protected async init() {
// Do nothing since cipher is already decoded
}
protected showFixOldAttachments(attachment: AttachmentView) {
return false;
}
}

View File

@@ -38,28 +38,6 @@
<br />
<small class="tw-text-xs">{{ currentCipher.subTitle }}</small>
</td>
<td bitCell>
<div *ngIf="currentCipher.hasAttachments">
<button
[bitMenuTriggerFor]="optionsMenu"
type="button"
buttonType="main"
bitIconButton="bwi-ellipsis-v"
appA11yTitle="{{ 'options' | i18n }}"
></button>
<bit-menu #optionsMenu>
<button
type="button"
bitMenuItem
appStopClick
(click)="viewAttachments(currentCipher)"
>
<i class="bwi bwi-fw bwi-paperclip" aria-hidden="true"></i>
{{ "attachments" | i18n }}
</button>
</bit-menu>
</div>
</td>
</tr>
</ng-template>
</bit-table>

View File

@@ -1,15 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit, ViewChild, ViewContainerRef } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service";
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { DialogService } from "@bitwarden/components";
import { CipherFormConfigService, DefaultCipherFormConfigService } from "@bitwarden/vault";
import { EmergencyAccessService } from "../../../emergency-access";
import { EmergencyAccessAttachmentsComponent } from "../attachments/emergency-access-attachments.component";
import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component";
@@ -20,56 +18,34 @@ import { EmergencyViewDialogComponent } from "./emergency-view-dialog.component"
})
export class EmergencyAccessViewComponent implements OnInit {
@ViewChild("attachments", { read: ViewContainerRef, static: true })
attachmentsModalRef: ViewContainerRef;
id: string;
id: EmergencyAccessId | null = null;
ciphers: CipherView[] = [];
loaded = false;
constructor(
private modalService: ModalService,
private router: Router,
private route: ActivatedRoute,
private emergencyAccessService: EmergencyAccessService,
private dialogService: DialogService,
) {}
ngOnInit() {
// eslint-disable-next-line rxjs-angular/prefer-takeuntil
this.route.params.subscribe((qParams) => {
if (qParams.id == null) {
return this.router.navigate(["settings/emergency-access"]);
}
async ngOnInit() {
const qParams = await firstValueFrom(this.route.params);
if (qParams.id == null) {
await this.router.navigate(["settings/emergency-access"]);
return;
}
this.id = qParams.id;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.load();
});
this.id = qParams.id;
this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(qParams.id);
this.loaded = true;
}
async selectCipher(cipher: CipherView) {
EmergencyViewDialogComponent.open(this.dialogService, {
cipher,
emergencyAccessId: this.id!,
});
return;
}
async load() {
this.ciphers = await this.emergencyAccessService.getViewOnlyCiphers(this.id);
this.loaded = true;
}
// FIXME PM-17747: This will also need to be replaced with the new AttachmentViewDialog
async viewAttachments(cipher: CipherView) {
await this.modalService.openViewRef(
EmergencyAccessAttachmentsComponent,
this.attachmentsModalRef,
(comp) => {
comp.cipher = cipher;
comp.emergencyAccessId = this.id;
},
);
}
}

View File

@@ -3,7 +3,7 @@
{{ title }}
</span>
<div bitDialogContent #dialogContent>
<app-cipher-view [cipher]="cipher"></app-cipher-view>
<app-cipher-view [emergencyAccessId]="emergencyAccessId" [cipher]="cipher"></app-cipher-view>
</div>
<ng-container bitDialogFooter>
<button bitButton type="button" buttonType="secondary" (click)="cancel()">

View File

@@ -4,6 +4,7 @@ import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EmergencyAccessId } from "@bitwarden/common/types/guid";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
import { CipherType } from "@bitwarden/common/vault/enums";
@@ -20,6 +21,7 @@ import { WebViewPasswordHistoryService } from "../../../../vault/services/web-vi
export interface EmergencyViewDialogParams {
/** The cipher being viewed. */
cipher: CipherView;
emergencyAccessId: EmergencyAccessId;
}
/** Stubbed class, premium upgrade is not applicable for emergency viewing */
@@ -59,6 +61,10 @@ export class EmergencyViewDialogComponent {
return this.params.cipher;
}
get emergencyAccessId(): EmergencyAccessId {
return this.params.emergencyAccessId;
}
cancel = () => {
this.dialogRef.close();
};

View File

@@ -1,22 +0,0 @@
<form [formGroup]="formGroup" [bitSubmit]="submit" class="tw-container">
<div *ngIf="loggingIn">
<i class="bwi bwi-spinner bwi-spin" title="{{ 'loading' | i18n }}" aria-hidden="true"></i>
{{ "loading" | i18n }}
</div>
<div *ngIf="!loggingIn">
<p bitTypography="body1">{{ "ssoLogInWithOrgIdentifier" | i18n }}</p>
<bit-form-field>
<bit-label>{{ "ssoIdentifier" | i18n }}</bit-label>
<input bitInput type="text" formControlName="identifier" appAutofocus />
</bit-form-field>
<hr />
<div class="tw-flex tw-gap-2">
<button type="submit" bitButton bitFormButton buttonType="primary" [block]="true">
{{ "logIn" | i18n }}
</button>
<a bitButton buttonType="secondary" routerLink="/login" [block]="true">
{{ "cancel" | i18n }}
</a>
</div>
</div>
</form>

View File

@@ -1,186 +0,0 @@
// FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore
import { Component, OnInit } from "@angular/core";
import { FormControl, FormGroup, Validators } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { first } from "rxjs/operators";
import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component";
import {
LoginStrategyServiceAbstraction,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
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 { 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 { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/key-management/master-password/abstractions/master-password.service.abstraction";
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";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service";
import { ToastService } from "@bitwarden/components";
import { PasswordGenerationServiceAbstraction } from "@bitwarden/generator-legacy";
@Component({
selector: "app-sso",
templateUrl: "sso-v1.component.html",
})
export class SsoComponentV1 extends BaseSsoComponent implements OnInit {
protected formGroup = new FormGroup({
identifier: new FormControl(null, [Validators.required]),
});
get identifierFormControl() {
return this.formGroup.controls.identifier;
}
constructor(
ssoLoginService: SsoLoginServiceAbstraction,
loginStrategyService: LoginStrategyServiceAbstraction,
router: Router,
i18nService: I18nService,
route: ActivatedRoute,
stateService: StateService,
platformUtilsService: PlatformUtilsService,
apiService: ApiService,
cryptoFunctionService: CryptoFunctionService,
environmentService: EnvironmentService,
passwordGenerationService: PasswordGenerationServiceAbstraction,
logService: LogService,
private orgDomainApiService: OrgDomainApiServiceAbstraction,
private validationService: ValidationService,
userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction,
configService: ConfigService,
masterPasswordService: InternalMasterPasswordServiceAbstraction,
accountService: AccountService,
toastService: ToastService,
) {
super(
ssoLoginService,
loginStrategyService,
router,
i18nService,
route,
stateService,
platformUtilsService,
apiService,
cryptoFunctionService,
environmentService,
passwordGenerationService,
logService,
userDecryptionOptionsService,
configService,
masterPasswordService,
accountService,
toastService,
);
this.redirectUri = window.location.origin + "/sso-connector.html";
this.clientId = "web";
}
async ngOnInit() {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
super.ngOnInit();
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.queryParams.pipe(first()).subscribe(async (qParams) => {
if (qParams.identifier != null) {
// SSO Org Identifier in query params takes precedence over claimed domains
this.identifierFormControl.setValue(qParams.identifier);
this.loggingIn = true;
await this.submit();
} else {
// Note: this flow is written for web but both browser and desktop
// redirect here on SSO button click.
// Check if email matches any claimed domains
if (qParams.email) {
// show loading spinner
this.loggingIn = true;
try {
if (await this.configService.getFeatureFlag(FeatureFlag.VerifiedSsoDomainEndpoint)) {
const response: ListResponse<VerifiedOrganizationDomainSsoDetailsResponse> =
await this.orgDomainApiService.getVerifiedOrgDomainsByEmail(qParams.email);
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);
}
this.loggingIn = false;
}
// Fallback to state svc if domain is unclaimed
const storedIdentifier = await this.ssoLoginService.getOrganizationSsoIdentifier();
if (storedIdentifier != null) {
this.identifierFormControl.setValue(storedIdentifier);
}
}
});
}
private handleGetClaimedDomainByEmailError(error: any): void {
if (error instanceof ErrorResponse) {
const errorResponse: ErrorResponse = error as ErrorResponse;
switch (errorResponse.statusCode) {
case HttpStatusCode.NotFound:
//this is a valid case for a domain not found
return;
default:
this.validationService.showError(errorResponse);
break;
}
}
}
submit = async () => {
if (this.formGroup.invalid) {
return;
}
const autoSubmit = (await firstValueFrom(this.route.queryParams)).identifier != null;
this.identifier = this.identifierFormControl.value;
await this.ssoLoginService.setOrganizationSsoIdentifier(this.identifier);
if (this.clientId === "browser") {
document.cookie = `ssoHandOffMessage=${this.i18nService.t("ssoHandOff")};SameSite=strict`;
}
try {
await Object.getPrototypeOf(this).submit.call(this);
} catch (error) {
if (autoSubmit) {
await this.router.navigate(["/login"]);
} else {
this.validationService.showError(error);
}
}
};
}

View File

@@ -23,7 +23,6 @@ import { DangerZoneComponent } from "../auth/settings/account/danger-zone.compon
import { DeauthorizeSessionsComponent } from "../auth/settings/account/deauthorize-sessions.component";
import { DeleteAccountDialogComponent } from "../auth/settings/account/delete-account-dialog.component";
import { ProfileComponent } from "../auth/settings/account/profile.component";
import { EmergencyAccessAttachmentsComponent } from "../auth/settings/emergency-access/attachments/emergency-access-attachments.component";
import { EmergencyAccessConfirmComponent } from "../auth/settings/emergency-access/confirm/emergency-access-confirm.component";
import { EmergencyAccessAddEditComponent } from "../auth/settings/emergency-access/emergency-access-add-edit.component";
import { EmergencyAccessComponent } from "../auth/settings/emergency-access/emergency-access.component";
@@ -42,7 +41,6 @@ import { TwoFactorSetupYubiKeyComponent } from "../auth/settings/two-factor/two-
import { TwoFactorSetupComponent } from "../auth/settings/two-factor/two-factor-setup.component";
import { TwoFactorVerifyComponent } from "../auth/settings/two-factor/two-factor-verify.component";
import { UserVerificationModule } from "../auth/shared/components/user-verification";
import { SsoComponentV1 } from "../auth/sso-v1.component";
import { UpdatePasswordComponent } from "../auth/update-password.component";
import { UpdateTempPasswordComponent } from "../auth/update-temp-password.component";
import { VerifyEmailTokenComponent } from "../auth/verify-email-token.component";
@@ -116,7 +114,6 @@ import { SharedModule } from "./shared.module";
DeleteAccountDialogComponent,
DomainRulesComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
@@ -146,7 +143,6 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponentV1,
TwoFactorSetupAuthenticatorComponent,
TwoFactorSetupDuoComponent,
TwoFactorSetupEmailComponent,
@@ -175,7 +171,6 @@ import { SharedModule } from "./shared.module";
DomainRulesComponent,
DynamicAvatarComponent,
EmergencyAccessAddEditComponent,
EmergencyAccessAttachmentsComponent,
EmergencyAccessComponent,
EmergencyAccessConfirmComponent,
EmergencyAccessTakeoverComponent,
@@ -206,7 +201,6 @@ import { SharedModule } from "./shared.module";
SetPasswordComponent,
SponsoredFamiliesComponent,
SponsoringOrgRowComponent,
SsoComponentV1,
TwoFactorSetupAuthenticatorComponent,
TwoFactorSetupDuoComponent,
TwoFactorSetupEmailComponent,

View File

@@ -25,7 +25,6 @@ export enum FeatureFlag {
GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor",
IdpAutoSubmitLogin = "idp-auto-submit-login",
InlineMenuPositioningImprovements = "inline-menu-positioning-improvements",
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
NotificationRefresh = "notification-refresh",
UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection",
MacOsNativeCredentialSync = "macos-native-credential-sync",
@@ -87,7 +86,6 @@ export const DefaultFeatureFlagValue = {
[FeatureFlag.GenerateIdentityFillScriptRefactor]: FALSE,
[FeatureFlag.IdpAutoSubmitLogin]: FALSE,
[FeatureFlag.InlineMenuPositioningImprovements]: FALSE,
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
[FeatureFlag.NotificationRefresh]: FALSE,
[FeatureFlag.UseTreeWalkerApiForPageDetailsCollection]: FALSE,
[FeatureFlag.MacOsNativeCredentialSync]: FALSE,

View File

@@ -12,3 +12,4 @@ export type SendId = Opaque<string, "SendId">;
export type IndexedEntityId = Opaque<string, "IndexedEntityId">;
export type SecurityTaskId = Opaque<string, "SecurityTaskId">;
export type NotificationId = Opaque<string, "NotificationId">;
export type EmergencyAccessId = Opaque<string, "EmergencyAccessId">;

View File

@@ -234,6 +234,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
this.organizationId = value !== "myVault" ? value : undefined;
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");
this.exportForm.get("format").setValue("json");
if (value === "myVault" && isExportAttachmentsEnabled) {
this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" });
}

View File

@@ -14,6 +14,7 @@
[cipher]="cipher"
[attachment]="attachment"
[checkPwReprompt]="true"
[emergencyAccessId]="emergencyAccessId"
></app-download-attachment>
</bit-item-action>
</ng-container>

View File

@@ -9,7 +9,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import {
@@ -41,6 +41,9 @@ import { DownloadAttachmentComponent } from "../../components/download-attachmen
export class AttachmentsV2ViewComponent {
@Input() cipher: CipherView;
// Required for fetching attachment data when viewed from cipher via emergency access
@Input() emergencyAccessId?: EmergencyAccessId;
canAccessPremium: boolean;
orgKey: OrgKey;

View File

@@ -76,7 +76,8 @@
<!-- ATTACHMENTS SECTION -->
<ng-container *ngIf="cipher.attachments">
<app-attachments-v2-view [cipher]="cipher"> </app-attachments-v2-view>
<app-attachments-v2-view [emergencyAccessId]="emergencyAccessId" [cipher]="cipher">
</app-attachments-v2-view>
</ng-container>
<!-- ITEM HISTORY SECTION -->

View File

@@ -15,7 +15,7 @@ import { isCardExpired } from "@bitwarden/common/autofill/utils";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId, CollectionId, UserId } from "@bitwarden/common/types/guid";
import { CipherId, CollectionId, EmergencyAccessId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -61,6 +61,9 @@ import { ViewIdentitySectionsComponent } from "./view-identity-sections/view-ide
export class CipherViewComponent implements OnChanges, OnDestroy {
@Input({ required: true }) cipher: CipherView | null = null;
// Required for fetching attachment data when viewed from cipher via emergency access
@Input() emergencyAccessId?: EmergencyAccessId;
activeUserId$ = getUserId(this.accountService.activeAccount$);
/**

View File

@@ -13,7 +13,7 @@ import { FileDownloadService } from "@bitwarden/common/platform/abstractions/fil
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer";
import { StateProvider } from "@bitwarden/common/platform/state";
import { OrganizationId } from "@bitwarden/common/types/guid";
import { EmergencyAccessId, OrganizationId } from "@bitwarden/common/types/guid";
import { OrgKey } from "@bitwarden/common/types/key";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -36,6 +36,9 @@ export class DownloadAttachmentComponent {
// When in view mode, we will want to check for the master password reprompt
@Input() checkPwReprompt?: boolean = false;
// Required for fetching attachment data when viewed from cipher via emergency access
@Input() emergencyAccessId?: EmergencyAccessId;
/** The organization key if the cipher is associated with one */
private orgKey: OrgKey | null = null;
@@ -68,6 +71,7 @@ export class DownloadAttachmentComponent {
const attachmentDownloadResponse = await this.apiService.getAttachmentData(
this.cipher.id,
this.attachment.id,
this.emergencyAccessId,
);
url = attachmentDownloadResponse.url;
} catch (e) {