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-12049] Remove usage of ActiveUserState from folder service #11880

Open
wants to merge 14 commits into
base: main
Choose a base branch
from

Conversation

gbubemismith
Copy link
Member

@gbubemismith gbubemismith commented Nov 6, 2024

🎟️ Tracking

https://bitwarden.atlassian.net/browse/PM-12049

📔 Objective

ActiveUserState is being deprecated. Remove usage from folderService and require userId to be passed.

📸 Screenshots

⏰ 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

…tate

Added extra test cases for encrypted folder and decrypted folders

Updated derived state to use decrypt with key

This comment has been minimized.

Copy link

codecov bot commented Nov 7, 2024

Codecov Report

Attention: Patch coverage is 40.95745% with 111 lines in your changes missing coverage. Please review.

Project coverage is 33.51%. Comparing base (ca839b3) to head (12a8501).
Report is 1 commits behind head on main.

✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
libs/common/src/platform/sync/core-sync.service.ts 0.00% 10 Missing ⚠️
.../src/vault/components/folder-add-edit.component.ts 0.00% 9 Missing ⚠️
...wser/src/vault/popup/settings/folders.component.ts 0.00% 7 Missing ⚠️
apps/cli/src/vault/create.command.ts 0.00% 7 Missing ⚠️
...ault/vault-filter/services/vault-filter.service.ts 0.00% 7 Missing ⚠️
...common/src/vault/services/folder/folder.service.ts 66.66% 7 Missing ⚠️
apps/cli/src/commands/edit.command.ts 0.00% 6 Missing ⚠️
apps/cli/src/commands/get.command.ts 0.00% 6 Missing ⚠️
apps/cli/src/vault/delete.command.ts 0.00% 6 Missing ⚠️
...ogs/bulk-move-dialog/bulk-move-dialog.component.ts 0.00% 6 Missing ⚠️
... and 16 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #11880      +/-   ##
==========================================
- Coverage   33.52%   33.51%   -0.01%     
==========================================
  Files        2859     2859              
  Lines       89434    89489      +55     
  Branches    17022    17033      +11     
==========================================
+ Hits        29979    29994      +15     
- Misses      57094    57129      +35     
- Partials     2361     2366       +5     

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


🚨 Try these New Features:

return [];
}

const decryptFolderPromises = folders.map((f) => f.decryptWithKey(userKey, encryptService));
Copy link
Member Author

Choose a reason for hiding this comment

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

ℹ️ @bitwarden/team-platform-dev changed folder decryption to use decryptWithKey instead of the decrypt method.


folders$(userId$: Observable<UserId>): Observable<Folder[]> {
return userId$.pipe(
takeWhile((userId) => userId != null),
Copy link
Member Author

@gbubemismith gbubemismith Nov 8, 2024

Choose a reason for hiding this comment

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

ℹ️ takeWhile operator to stop folder state observations when userId becomes invalid (during logout)

@gbubemismith gbubemismith marked this pull request as ready for review November 8, 2024 17:52
@gbubemismith gbubemismith requested review from a team as code owners November 8, 2024 17:52
this.folderViews$ = this.decryptedFoldersState.state$;
folderViews$(userId$: Observable<UserId>): Observable<FolderView[]> {
return userId$.pipe(
takeWhile((userId) => userId != null),
Copy link
Member Author

@gbubemismith gbubemismith Nov 8, 2024

Choose a reason for hiding this comment

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

ℹ️ takeWhile operator to stop folder state observations when userId becomes invalid (during logout)

audreyality
audreyality previously approved these changes Nov 12, 2024
Copy link
Member

@shane-melton shane-melton left a comment

Choose a reason for hiding this comment

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

Nice work on this so far!

I mostly just left a couple thoughts/comments on my concern with mixing Observable<UserId> and UserId arguments to the FolderService. They're not hard change requests, but I think it something we should consider / discuss further.

Also, I do think we'll need to introduce the same UserId requirement to the FolderApiService to ensure we are always using the same userId throughout a submission.

Copy link
Member

Choose a reason for hiding this comment

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

💭 We may need to update these methods to also accept a userId to ensure the userId is consistent in the calling location and within the api service.

For instance, the AddEditFolderDialog component has the following snippet in its save() method:

const activeUserId = await firstValueFrom(this.accountService.activeAccount$);
const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId.id);
const folder = await this.folderService.encrypt(this.folder, userKey); 
// The activeAccount$ could have potentially changed during encryption, and the API service would use the updated userId
await this.folderApiService.save(folder);

The CLI edit and create commands have similar saving behavior.

@@ -62,7 +64,7 @@ export class IndividualVaultExportService
const promises = [];

promises.push(
this.folderService.getAllDecryptedFromState().then((folders) => {
firstValueFrom(this.folderService.folderViews$(this.activeUserId$)).then((folders) => {
Copy link
Member

Choose a reason for hiding this comment

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

💭 Here's an example of where we would want folderViews$ to use a single UserId instead of an Observable<UserId>.

The cipherService.getAllDecrypted() call below will eventually be updated to a cipherViews$(userId) method. Now, if both ...Views$ observables take their own activeUserId$ observable as an input, there is a chance the activeUserId$ could emit in-between the calls to folderViews$() and cipherViews$(). If this were to happen, the folders and ciphers would belong to different users.

If we instead, set const activeUserId = await firstvalueFrom(this.activeUserId$) at the top of the method and pass it into each ...Views$(userId) from both services we can ensure the same user is being referenced by both.


this.folderViews$ = this.decryptedFoldersState.state$;
folderViews$(userId$: Observable<UserId>): Observable<FolderView[]> {
Copy link
Member

Choose a reason for hiding this comment

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

💭 I'm hesitant to update this interface to accept an Observable<UserId>. There is a chance users of the service could be combining the results of folderViews$ with other service observables. For example:

combineLatest([
  folderService.folderViews$(accountService.activeUserId$), 
  cipherService.cipherViews$(accountService.activeUserId$)
])

If this happens and activeUserId$ emits, it could lead to folderView$() emitting before cipherViews$() (because folders are typically fewer/smaller so faster to decrypt than ciphers). If that happens then the combineLatest will emit with [newUserIdFolderViews, oldUserIdCipherViews] and the consumer could be inadvertently mixing user data.

By requiring a specific userId we help protect consumers / encourage them to ensure they have an unchanging userId for all of their services.

Open to other's thoughts on this. There was a related conversation in Slack here and a good StackBlitz example of the potential issue from Justin.

@@ -58,29 +63,29 @@ export class FolderService implements InternalFolderServiceAbstraction {
return folder;
}

async get(id: string): Promise<Folder> {
const folders = await firstValueFrom(this.folders$);
async get(id: string, userId$: Observable<UserId>): Promise<Folder> {
Copy link
Member

Choose a reason for hiding this comment

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

💭 🎨 We're immediately taking the firstValueFrom this userId$ observable (via this.folders$()). Imo it would be more clear to simply accept an unchanging UserId.

}

async upsert(folderData: FolderData | FolderData[]): Promise<void> {
await this.encryptedFoldersState.update((folders) => {
async upsert(folderData: FolderData | FolderData[], userId: UserId): Promise<void> {
Copy link
Member

Choose a reason for hiding this comment

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

💭 This makes sense to me. It implies that the user of the service knows they're updating a specific user's folder and not just the latest from the activeUserId$ stream. But, now it seems kind of odd that we have some methods taking UserId and others taking Observable<UserId>. It makes me more inclined to be consistent and use UserId everywhere. What do you think?

) {
this.folders$ = this.folderService.folderViews$.pipe(
this.folders$ = this.folderService.folderViews$(this.activeUserId$).pipe(
Copy link
Member

Choose a reason for hiding this comment

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

💭 I'm not sure what we gain by making the folderViews$ reactive to / depend on the activeUserId$.

If the acitveUserId$ changes, we're almost certainly navigating away to another page (lock/logout) and the view will be destroyed very soon after. To avoid needing to await a firstValueFrom(this.activeUserId$) we could pipe/switchMap to the folderService.folderViews(activeUserId) instead.

@gbubemismith gbubemismith requested a review from a team as a code owner November 19, 2024 15:36
@gbubemismith
Copy link
Member Author

Nice work on this so far!

I mostly just left a couple thoughts/comments on my concern with mixing Observable<UserId> and UserId arguments to the FolderService. They're not hard change requests, but I think it something we should consider / discuss further.

Also, I do think we'll need to introduce the same UserId requirement to the FolderApiService to ensure we are always using the same userId throughout a submission.

Yeah I agree with simplifying it to a single user id. The simpler userId approach makes things more predictable 👍🏼

@@ -256,7 +259,10 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {

private handleImportInit() {
// Filter out the no folder-item from folderViews$
this.folders$ = this.folderService.folderViews$.pipe(
this.folders$ = this.activeUserId$.pipe(
Copy link
Member Author

@gbubemismith gbubemismith Nov 19, 2024

Choose a reason for hiding this comment

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

@audreyality, does folders$ need to react to user changes in this component or we could simplify since the component would be destroyed during user change

const activeUserId = await firstValueFrom(this.activeUserId$);
this.folders$ = this.folderService.folderViews(userId)
  .pipe(map(folders => folders.filter(f => f.id != null)));

Copy link
Contributor

@audreyality audreyality Nov 19, 2024

Choose a reason for hiding this comment

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

There's no guarantee that a component will be destroyed when the user changes; that's just how it works right now. I'd like to keep the complexity since observables are more resilient to change than promises.

In particular, many of our components are going to stop querying the active user and instead be handed a specific userId. That requires handling angular change events; the component isn't recreated.

It's worth noting that the main reason why this comes up is because this is library code. Applications make specific guarantees about construction and destruction. Libraries do not.

Copy link
Contributor

@addisonbeck addisonbeck left a comment

Choose a reason for hiding this comment

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

LGTM, but a review from @justindbaur would be smart

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.

5 participants