Skip to content

Conversation

@aj-rosado
Copy link
Contributor

🎟️ Tracking

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

📔 Objective

Hide the accounts that contain an organization that does not allow Individual vault export on the Account Selector.

In order to achieve that and to ensure skipping the selection screen, when taking into consideration accounts that cannot export, some changes were needed to make

Saving account exportable info into UserState (in order to RootNav know which screen to navigate to)
Added getUserPolicies to PolicyManager that will return all the policies associated with the given userId (in order to get data for the iven userId and not only the current active)

VaultSyncManager re ordered to ensure when UserState is updated policies are also updated
SelectAccount verifying if account isExportable
Added more info to VerifyPasswordNavigation in order to properly know when to display the back button.

📸 Screenshots

Two accounts scenario

Screen.Recording.2025-10-27.at.12.48.35.mov

More than two accounts

Screen.Recording.2025-10-27.at.12.54.03.mov

⏰ 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

Added getUserPolicies to PolicyManager that will return all the policies associated with the given userId

VaultSyncManager re ordered to ensure when UserState is updated policies are also updated
SelectAccount verifying if account isExportable
Added more info to VerifyPasswordNavigation
@github-actions
Copy link
Contributor

github-actions bot commented Oct 28, 2025

Logo
Checkmarx One – Scan Summary & Detailsa330ac93-a577-4426-9b1d-58cb7b40b0f8

Fixed Issues (2)

Great job! The following issues were fixed in this Pull Request

Severity Issue Source File / Package
MEDIUM Use_of_Hardcoded_Password /app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt: 760
MEDIUM Use_of_Hardcoded_Password /app/src/test/kotlin/com/x8bit/bitwarden/data/vault/repository/VaultRepositoryTest.kt: 828

@codecov
Copy link

codecov bot commented Oct 28, 2025

Codecov Report

❌ Patch coverage is 94.44444% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 84.82%. Comparing base (562b48d) to head (f5dc13a).
⚠️ Report is 25 commits behind head on main.

Files with missing lines Patch % Lines
...itwarden/data/auth/manager/UserStateManagerImpl.kt 83.33% 1 Missing ⚠️
...twarden/data/vault/manager/VaultSyncManagerImpl.kt 66.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #6089      +/-   ##
==========================================
+ Coverage   84.74%   84.82%   +0.07%     
==========================================
  Files         737      721      -16     
  Lines       55439    52824    -2615     
  Branches     7655     7667      +12     
==========================================
- Hits        46983    44808    -2175     
+ Misses       5775     5328     -447     
- Partials     2681     2688       +7     

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

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aj-rosado aj-rosado marked this pull request as ready for review October 28, 2025 15:41
userId = userId,
type = type,
policies = authDiskSource.getPolicies(userId = userId),
) ?: emptyList()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we use orEmpty()?
Also, this should use chain formatting

    override fun getUserPolicies(
        userId: String,
        type: PolicyTypeJson,
    ): List<SyncResponseJson.Policy> =
        this
            .filterPolicies(
                userId = userId,
                type = type,
                policies = authDiskSource.getPolicies(userId = userId),
            )
            .orEmpty()

private fun existingPolicies(
userId: String,
policyType: PolicyTypeJson,
): List<SyncResponseJson.Policy>? = policyManager.getUserPolicies(
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why is this return type nullable when getUserPolicies returns a non-null value?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

had a different implementation that was nullable, not necessary anymore. Removed

userId = userId,
policies = syncResponse.policies.orEmpty(),
)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Is there a functional reason this was moved?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, the UserState was being updated before the policies were updated, so the existingPolicies wasn't returning the most recent data.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can you add a comment explaining that order matters here

navController.navigateToVerifyPassword(
userId = currentState.userId,
navOptions = rootNavOptions,
hasOtherAccounts = false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

So this can never be true?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is on the RootNavState.CredentialExchangeExportSkipAccountSelection scenario and this is only called when only exists 1 valid account, so yes

vaultLockManager = vaultLockManager,
dispatcherManager = dispatcherManager,
policyManager = policyManager,

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we drop this blank line

isBiometricsEnabledProvider: (userId: String) -> Boolean,
vaultUnlockTypeProvider: (userId: String) -> VaultUnlockType,
isDeviceTrustedProvider: (userId: String) -> Boolean,
getUserPolicies: (userId: String, policy: PolicyTypeJson) -> List<SyncResponseJson.Policy>?,
Copy link
Collaborator

Choose a reason for hiding this comment

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

This return type should not be nullable.

Added test for getUserPolicies
Removed optional on getUserPolicies
userId = USER_ID,
type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
).any())
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please format this

        assertTrue(
            policyManager
                .getUserPolicies(
                    userId = USER_ID,
                    type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
                )
                .any()
        )

Copy link
Collaborator

Choose a reason for hiding this comment

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

Better yet, can we assert it gets the correct value back

assertEquals(
    policyManager.getUserPolicies(
        userId = USER_ID,
        type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
    ),
    // actual value
)

userId = USER_ID,
type = PolicyTypeJson.PERSONAL_OWNERSHIP,
)
.isEmpty())
Copy link
Collaborator

Choose a reason for hiding this comment

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

Formatting, or assert the actual value:

assertEquals(
    policyManager.getUserPolicies(
        userId = USER_ID,
        type = PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
    ),
    emptyList<...>(),
)

isUsingKeyConnector = false,
onboardingStatus = OnboardingStatus.NOT_STARTED,
firstTimeState = FirstTimeState(showImportLoginsCard = true),
isExportable = false,
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

val actualItem = awaitItem()
assertEquals(userState, actualItem)

assertEquals(false, actualItem?.accounts[0]?.isExportable)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this line do anything?

If the isExportable value was true, wouldn't the assertion above have failed?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

it does because we are checking if the organizations id match the policy orgId, but now that I think about it, as we are returning the user policies, it seems to be a redundant check 🤔

Copy link
Contributor Author

@aj-rosado aj-rosado Oct 30, 2025

Choose a reason for hiding this comment

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

Updated the implementation, now it does not check with organization as it was redundant. Only verifies if the user has the policy.
Removed the isExportable assert

userId,
PolicyTypeJson.PERSONAL_OWNERSHIP,
)
.orEmpty()
Copy link
Collaborator

Choose a reason for hiding this comment

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

You shouldn't need this orEmpty() anymore.

Reworked some UserStateManager and PolicyManager tests
val hasPersonalOwnershipRestrictedOrg = getUserPolicies(
userId,
PolicyTypeJson.PERSONAL_OWNERSHIP,
).isNotEmpty()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we fix this formatting

val hasPersonalOwnershipRestrictedOrg = getUserPolicies(
    userId,
    PolicyTypeJson.PERSONAL_OWNERSHIP,
)
    .isNotEmpty()

val hasPersonalVaultExportRestrictedOrg = getUserPolicies(
userId,
PolicyTypeJson.DISABLE_PERSONAL_VAULT_EXPORT,
).isNotEmpty()
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same thing here.

We have docs on this here

Added test to verify if policy isExportable respects if policy is enabled
@aj-rosado aj-rosado added this pull request to the merge queue Oct 31, 2025
Merged via the queue into main with commit d07b119 Oct 31, 2025
13 checks passed
@aj-rosado aj-rosado deleted the PM-27120/cxp-hide-user-account-remove-individual-export branch October 31, 2025 10:39
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.

3 participants