Skip to content

Conversation

@harCamConsulting
Copy link
Contributor

@harCamConsulting harCamConsulting commented Aug 5, 2025

Description

Changed the roles parameter on user_role to be able to take a list.
Also added a new parameter "remove_unlisted" which will default to false. This should mean the change is non-breaking for the moment, as the role list can also take a string, which will auto convert to list of one item.
In later versions (perhaps a major version?) The default should probably for remove_unlisted should probably change to true to make this module be more idempotent - but it may break compatibility with previously written tasks.

How Has This Been Tested?

Manually tested locally - but needs testing against unit tests.
Have now added unit tests for new functionality, but this also changed the output of module - breaking change maybe?
Added functionality now to give a compatibility mode where calling with using old parameters gives old style output. Using new "roles" parameter gives new output including diff.

Types of changes

  • Bug fix (non-breaking change which fixes an issue) - Fixes #
  • New feature (non-breaking change which adds functionality)

Checklist:

@codecov-commenter
Copy link

codecov-commenter commented Aug 5, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 89.71%. Comparing base (047e0db) to head (88f5578).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #324      +/-   ##
==========================================
- Coverage   93.77%   89.71%   -4.06%     
==========================================
  Files          68       55      -13     
  Lines        2392     1536     -856     
==========================================
- Hits         2243     1378     -865     
- Misses        149      158       +9     

☔ 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.

@harCamConsulting
Copy link
Contributor Author

I've now added some unit tests to test the new functionality of the lists, but to properly test, I've had to change the output of the module - possibly this could be a breaking change? The existing output was just the output of the add- or the remove- command which didn't really make sense when it was possibly doing multiple adds and/or removes anyway

@lowlydba lowlydba requested review from Copilot and lowlydba August 16, 2025 21:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Updates the user_role module to accept a list of roles instead of just a single role string, enhancing functionality while maintaining backward compatibility through aliases. Adds an optional parameter to remove unlisted roles for better idempotency control.

  • Converts the role parameter to accept a list while maintaining backward compatibility via aliases
  • Adds remove_unlisted parameter to optionally remove roles not specified in the list
  • Updates module logic to handle multiple role operations and provides detailed output of role membership changes

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
plugins/modules/user_role.py Updates module documentation to reflect list-based role parameter and new remove_unlisted option
plugins/modules/user_role.ps1 Implements core logic for handling multiple roles and optional removal of unlisted roles
tests/integration/targets/user_role/tasks/main.yml Adds comprehensive test cases for new list functionality and role removal behavior
galaxy.yml Bumps version to 2.6.2 for the new feature
changelogs/changelog.yaml Documents the enhancement in the changelog
sqlserver Adds symlink file (likely for development setup)

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

@lowlydba
Copy link
Owner

@harCamConsulting Thanks for opening the PR!

I am concerned this approach may cause unnecessary breaking changes later though, since its not quite the same as the add/remove/set pattern mentioned in the original issue, and that is likely the goal end state for this sort of feature. I fear it might create more difficulty to add the ability to add lists, but not remove via lists - for example.

I don't think we need to copy the exact code used in the AD collection example, just use the same parameters for the functionality - if that makes it any easier.

If we can add those as purely new parameters, we can keep the old ones but mark them as deprecated, and then leave the backwards compatibility until a new major version release.

@harCamConsulting
Copy link
Contributor Author

Yeah, that's fair. I'll keep working on it.

@harCamConsulting
Copy link
Contributor Author

ok - sorry for the push spam there, but I think I've got the code back to working and passing the CI checks again. Although, the "|grp1" checks don't seem to pass, but I think it was like that before anyway?
I've implemented the add/remove/set functionality under the "roles" parameter versus the old role (singular) parameter but concerned that they're too closely named and may cause confusion. Happy to take direction there and rename it to something else.

I also implemented calling the module without either of the role or roles parameter to give back a list of current roles for the user (i.e. kinda like a facts module), along with a few more tests which use that.

@lowlydba
Copy link
Owner

lowlydba commented Sep 5, 2025

ok - sorry for the push spam there, but I think I've got the code back to working and passing the CI checks again. Although, the "|grp1" checks don't seem to pass, but I think it was like that before anyway?

I have fixed almost all the tests (except for the dlevel Sanity, which is just a new upstream breaking change), so we should be expecting these to work. I'll have to spend some time looking at the errors (which I will try to find the time for soon) but we'll need these to be green to merge.

EDIT: All tests should be passing on the main branch now

@harCamConsulting
Copy link
Contributor Author

ok that's better, I was handling arrays weird, which caused them to come out double nested in the output.

Still keen to hear thoughts on the naming of "role" vs "roles" possible confusion or if there might be a better name to use.

@lowlydba
Copy link
Owner

ok that's better, I was handling arrays weird, which caused them to come out double nested in the output.

Still keen to hear thoughts on the naming of "role" vs "roles" possible confusion or if there might be a better name to use.

Loving the green tests! Its on my radar to carve out time for another review of this, thanks for hanging in there

@harCamConsulting
Copy link
Contributor Author

Just wanted to give this a bump to see if there's anything else I can do here?

@lowlydba
Copy link
Owner

lowlydba commented Jan 8, 2026

Just wanted to give this a bump to see if there's anything else I can do here?

You're all good. I sincerely appreciate your patience and persistence on this. I finally have had some time to review, and it all looks good. I'll do a final review with Copilot, and then ship it if things are good.

Thank you @harCamConsulting !

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 14 comments.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 15 comments.

$output = New-Object -TypeName PSCustomObject -Property $outputProps
}

try {
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

In non-compatibility mode when no changes occur, the code creates an output with just 'roleMembership' but no 'diff' (lines 198-204). However, when changes do occur, it includes both 'roleMembership' and 'diff'. This is good idempotent behavior. However, in the query-only mode (no role or roles specified), the code should also return the current role membership. The current logic doesn't handle the query-only case where no roles are specified to add/remove/set - it would skip to line 207 with $output potentially undefined if not in compatibility mode.

Suggested change
try {
try {
# In non-compatibility (roles dict) mode, if no output was created earlier (e.g. query-only),
# return the current role membership without a diff.
if (-not $compatibilityMode -and $null -eq $output) {
try {
$membershipObjects = Get-DbaDbRoleMember @commonParamSplat -IncludeSystemUser $true | Where-Object { $_.UserName -eq $username }
$currentRoleMembership = [array]($membershipObjects.role | Sort-Object)
if ($null -eq $currentRoleMembership) { $currentRoleMembership = @() }
}
catch {
$module.FailJson("Failure getting current role membership: $($_.Exception.Message)", $_)
}
if ($null -eq $outputProps) { $outputProps = @{} }
$outputProps.roleMembership = $currentRoleMembership
$output = New-Object -TypeName PSCustomObject -Property $outputProps
}

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

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

@harCamConsulting I think this comment makes sense, can you add this query-mode logic in?

Comment on lines 84 to 93
# Ensure that when using the 'roles' parameter directly, at least one operation is specified.
if (-not $compatibilityMode) {
$hasSet = ($null -ne $roles['set'] -and @($roles['set']).Count -gt 0)
$hasAdd = ($null -ne $roles['add'] -and @($roles['add']).Count -gt 0)
$hasRemove = ($null -ne $roles['remove'] -and @($roles['remove']).Count -gt 0)

if (-not ($hasSet -or $hasAdd -or $hasRemove)) {
$module.FailJson("When using the 'roles' parameter, you must specify at least one of: roles.set, roles.add, or roles.remove.")
}
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The validation check at lines 84-93 will execute even when neither role nor roles parameters are provided (the "get current role membership" mode documented in the RETURN section). When a user wants to simply query the current role membership without providing either parameter, this check will incorrectly fail. The check should only apply when the roles parameter is explicitly provided by the user.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

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

@harCamConsulting Can you add a check in line 88 to ensure $roles is defined in addition to the compat mode check?

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 6 comments.

Comment on lines +87 to 95
# Ensure that when using the 'roles' parameter directly, at least one operation is specified.
if (-not $compatibilityMode) {
$hasSet = ($null -ne $roles['set'] -and @($roles['set']).Count -gt 0)
$hasAdd = ($null -ne $roles['add'] -and @($roles['add']).Count -gt 0)
$hasRemove = ($null -ne $roles['remove'] -and @($roles['remove']).Count -gt 0)

if (-not ($hasSet -or $hasAdd -or $hasRemove)) {
$module.FailJson("When using the 'roles' parameter, you must specify at least one of: roles.set, roles.add, or roles.remove.")
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The validation logic has multiple issues:

  1. Line 89 checks @($roles['set']).Count -gt 0, which prevents using roles.set: [] to remove all roles (a documented feature in the tests and documentation).

  2. The validation doesn't account for "query mode" where neither 'role' nor 'roles' operations are specified (as tested at lines 198 and 224 of the integration tests). In this case, all three $hasSet, $hasAdd, and $hasRemove would be false, causing the module to fail incorrectly.

The fix should:

  • Change line 89 to $hasSet = $null -ne $roles['set'] (without the Count check) to allow empty arrays
  • Add logic to detect and allow query mode, where the module simply returns current role membership without modification

Copilot uses AI. Check for mistakes.
Comment on lines +123 to +150
if ($null -ne $roles['set']) {
$comparison = Compare-Object $existingRoleMembership ([array]$roles['set'])
if ($null -eq $comparison) {
$rolesToAdd = @()
$rolesToRemove = @()
}
else {
$rolesToAdd = ( $comparison | Where-Object { $_.SideIndicator -eq '=>' } ).InputObject
$rolesToRemove = ( $comparison | Where-Object { $_.SideIndicator -eq '<=' } ).InputObject
}
}
else {
$comparisonAdd = Compare-Object $existingRoleMembership ([array]$roles['add'])
if ($null -eq $comparisonAdd) {
$rolesToAdd = @()
}
else {
$rolesToAdd = ( $comparisonAdd | Where-Object { $_.SideIndicator -eq '=>' } ).InputObject
}

$comparisonRemove = Compare-Object $existingRoleMembership ([array]$roles['remove']) -IncludeEqual
if ($null -eq $comparisonRemove) {
$rolesToRemove = @()
}
else {
$rolesToRemove = ( $comparisonRemove | Where-Object { $_.SideIndicator -eq '==' } ).InputObject
}
}
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The module allows specifying roles.set simultaneously with roles.add or roles.remove, but the logic at line 123 gives precedence to roles.set, effectively ignoring any add or remove specifications. This could lead to confusing behavior where users expect add/remove to be applied in addition to set.

Consider either:

  1. Making set mutually exclusive with add/remove in the parameter spec
  2. Documenting that set takes precedence and add/remove are ignored when set is provided
  3. Applying add/remove operations after the set operation

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

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

@harCamConsulting This makes sense to me, any of the options given seem good

description:
- If called with role, then data is output from the C(Remove-DbaDbRoleMember), (Get-DbaDbRoleMember), or C(Add-DbaDbRoleMember) functions.
- If called with roles, then data returned is roleMembership, which is an array of roles that the user is now a member of.
- If called without either role or roles, then the data returned is roleMembership which is the user's current list of roles.
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The documentation states that when called without either role or roles, the data returned is roleMembership with the user's current list of roles. However, the code at lines 88-95 will fail with an error message when neither parameter is provided (in non-compatibility mode). This is inconsistent with the documented behavior shown in the test cases at lines 198 and 224.

The documentation should either be updated to remove this statement, or the code should be fixed to support query-only mode.

Suggested change
- If called without either role or roles, then the data returned is roleMembership which is the user's current list of roles.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

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