Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-6991] Improve reactivity of cipherViews$ observable in cipher service #11141

Merged
merged 13 commits into from
Oct 10, 2024

Conversation

shane-melton
Copy link
Member

@shane-melton shane-melton commented Sep 18, 2024

🎟️ Tracking

PM-6991

📔 Objective

We have run into race conditions where the vault would attempt to call cipherService.getAllDecrypted() before the sync had completed. This would lead to an empty list being returned and showing as an empty vault to the user until they refreshed.

This PR updates the existing cipherViews$ observable to be reactive to ciphers$ and localData$ so that whenever either of those streams update, it will internally call and return the value of getAllDecrypted() for any subscribers to cipherViews$. ciphers$ will always emit after a sync completes so this ensures and subscribers to cipherViews$ will be notified of an update list of decrypted ciphers.

With that change, the individual web vault and vault filter service now utilize the cipherViews$ observable instead of wrapping getAllDecrypted() in our asyncToObservable() helper.

Note

There is still plenty of room for improvement here. Namely, requiring a consumers to provide a userId when fetching decrypted ciphers instead of relying on the activeUserId internally. But, for now, this gets us one step closer to making the cipher service observable based.

Also, I left the Browser's Vault filter component alone as it already has some safe guards to avoid the race condition and it is being deprecated with the Browser refresh.

⏰ Reminders before review

  • Contributor guidelines followed
  • All formatters and local linters executed and passed
  • Written new unit and / or integration tests where applicable
  • Protected functional changes with optionality (feature flags)
  • Used internationalization (i18n) for all UI strings
  • CI builds passed
  • Communicated to DevOps any deployment requirements
  • Updated any necessary documentation (Confluence, contributing docs) or informed the documentation team

🦮 Reviewer guidelines

  • 👍 (:+1:) or similar for great changes
  • 📝 (:memo:) or ℹ️ (:information_source:) for notes or general info
  • ❓ (:question:) for questions
  • 🤔 (:thinking:) or 💭 (:thought_balloon:) for more open inquiry that's not quite a confirmed issue and could potentially benefit from discussion
  • 🎨 (:art:) for suggestions / improvements
  • ❌ (:x:) or ⚠️ (:warning:) for more significant problems or concerns needing attention
  • 🌱 (:seedling:) or ♻️ (:recycle:) for future improvements or indications of technical debt
  • ⛏ (:pick:) for minor or nitpick changes

Copy link
Contributor

github-actions bot commented Sep 18, 2024

Logo
Checkmarx One – Scan Summary & Detailsf9ba23eb-1c96-4085-873f-d565411bb4d0

New Issues

Severity Issue Source File / Package Checkmarx Insight
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/password-generator.component.html: 8 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/username-generator.component.html: 45 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/username-generator.component.html: 30 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/username-generator.component.html: 35 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/username-generator.component.html: 30 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/username-generator.component.html: 40 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/password-generator.component.html: 37 Attack Vector
MEDIUM Angular_Improper_Type_Pipe_Usage /libs/tools/generator/components/src/password-generator.component.html: 31 Attack Vector
MEDIUM Client_Privacy_Violation /apps/web/src/app/tools/reports/pages/weak-passwords-report.component.html: 19 Attack Vector
MEDIUM Client_Privacy_Violation /libs/tools/generator/components/src/password-generator.component.html: 8 Attack Vector
MEDIUM Client_Privacy_Violation /libs/tools/generator/components/src/password-generator.component.html: 8 Attack Vector
MEDIUM Client_Privacy_Violation /libs/tools/generator/components/src/credential-generator.component.html: 16 Attack Vector
MEDIUM Client_Privacy_Violation /libs/tools/generator/components/src/password-generator.component.html: 14 Attack Vector
MEDIUM Client_Privacy_Violation /libs/tools/generator/components/src/username-generator.component.html: 3 Attack Vector

Fixed Issues

Severity Issue Source File / Package
MEDIUM Client_Privacy_Violation /libs/tools/generator/components/src/username-generator.component.html: 3

Copy link

codecov bot commented Sep 18, 2024

Codecov Report

Attention: Patch coverage is 33.33333% with 10 lines in your changes missing coverage. Please review.

Project coverage is 33.16%. Comparing base (c221efd) to head (d34147d).
Report is 1 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
libs/common/src/vault/services/cipher.service.ts 30.76% 8 Missing and 1 partial ⚠️
.../src/app/vault/individual-vault/vault.component.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11141      +/-   ##
==========================================
- Coverage   33.18%   33.16%   -0.03%     
==========================================
  Files        2779     2779              
  Lines       86226    86223       -3     
  Branches    16419    16420       +1     
==========================================
- Hits        28618    28597      -21     
- Misses      55341    55360      +19     
+ Partials     2267     2266       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@gbubemismith
Copy link
Member

Thanks for working on this, @shane-melton; this is a significant improvement. The last time I did something similar, I noticed the cipherview$ didn't respond well when a new item was created. Did you try this out?

@shane-melton
Copy link
Member Author

the cipherview$ didn't respond well when a new item was created. Did you try this out?

@gbubemismith Yep! This works as expected here, because the source observable is ciphers$ which emits whenever an encrypted cipher is added/updated/deleted, it'll trigger cipherViews$ to re-evaluate and call getAllDecrypted() with the newly added cipher.

It also uses merge() with localData$ to be sure we update the cipherViews$ whenever the lastUsedDate or lastLaunchedDate are updated for a given cipher. So, for example, if a user autofills a cipher in browser it is moved to the top of the autofill list as expected.

// Decrypted ciphers depend on both ciphers and local data and need to be updated when either changes
this.cipherViews$ = merge(this.ciphers$, this.localData$).pipe(
switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())),
share(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ Why aren't we using a shareReplay(1)? My understanding is using shareReplay would mean VaultFilterComponent, VaultComponent and any other subscriber would get the most recent ciphers even if they subscribe at different times

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good point!

My thinking was to use share to avoid caching the decrypted ciphers in another spot that needed to be cleared from memory on lock/logout (it was before I had forceCipherViews$). And repeated calls to getAllDecrypted(), even by late subscribers, will still return a copy of the decrypted ciphers stored in state (internally it checks the decryptedCipherState first).

But, thinking about it again, the share operator will never allow getAllDecrypted() to be called again unless ciphers$ or localData$ emit again, so any late subscribers will just be stuck waiting.

I'll update to use shareReplay() like you suggest and we already have the clear issue taken care of with forceCipherViews$.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gbubemismith
gbubemismith previously approved these changes Sep 19, 2024
Copy link
Member

@gbubemismith gbubemismith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good 🚀


this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {}));
// First wait for ciphersExpectingUpdate to be false before emitting ciphers
this.ciphers$ = this.ciphersExpectingUpdate.state$.pipe(
skipWhile((expectingUpdate) => expectingUpdate),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ This was not having any impact on behavior because ciphersExpectingUpdate starts with a default value of false (see the removed DeriveDefinition above). And because it started with false the skipWhile passes immediately and is never called again.

@@ -279,7 +279,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));

const ciphers$ = combineLatest([
this.cipherService.cipherViews$,
this.cipherService.cipherViews$.pipe(filter((c) => c !== null)),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ Filtering out null values here prevents flashing the Empty vault screen while ciphers are being decrypted. If there are truthfully no ciphers in the vault (e.g. a new account) the cipherViews$ will emit an empty array instead and then we'll see the empty vault.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼 Thanks for this fix. I noticed that weird delay

Copy link
Member

@gbubemismith gbubemismith left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the improved changes

@@ -279,7 +279,7 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));

const ciphers$ = combineLatest([
this.cipherService.cipherViews$,
this.cipherService.cipherViews$.pipe(filter((c) => c !== null)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍🏼 Thanks for this fix. I noticed that weird delay

@shane-melton shane-melton merged commit 9d16355 into main Oct 10, 2024
64 checks passed
@shane-melton shane-melton deleted the vault/pm-6991/refactor-cipher-service-to-rxjs branch October 10, 2024 21:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants