-
Notifications
You must be signed in to change notification settings - Fork 1.3k
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
base: main
Are you sure you want to change the base?
Conversation
…tate Added extra test cases for encrypted folder and decrypted folders Updated derived state to use decrypt with key
This comment has been minimized.
This comment has been minimized.
Codecov ReportAttention: Patch coverage is
✅ All tests successful. No failed tests found. 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. 🚨 Try these New Features:
|
return []; | ||
} | ||
|
||
const decryptFolderPromises = folders.map((f) => f.decryptWithKey(userKey, encryptService)); |
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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)
this.folderViews$ = this.decryptedFoldersState.state$; | ||
folderViews$(userId$: Observable<UserId>): Observable<FolderView[]> { | ||
return userId$.pipe( | ||
takeWhile((userId) => userId != null), |
There was a problem hiding this comment.
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)
There was a problem hiding this 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.
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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[]> { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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.
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( |
There was a problem hiding this comment.
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)));
There was a problem hiding this comment.
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.
There was a problem hiding this 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
🎟️ Tracking
https://bitwarden.atlassian.net/browse/PM-12049
📔 Objective
ActiveUserState
is being deprecated. Remove usage fromfolderService
and require userId to be passed.📸 Screenshots
⏰ Reminders before review
🦮 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