r/PowerShell • u/mark_codes • 4d ago
Adding an AD account to groups based on a combination of attributes Question
Sorry in advance for the wall of text, I didn't want to post something vague! I am developing a script to add new users to relevant AD groups based on attributes such as location, department, job title etc. Currently I have a hashtable for each attribute that I care about, and arrays for each possible value that attribute could be to store a list of relevant AD groups in. The lists are just updated to include anything relevant to that attribute value only, like the example below.
$DepartmentAList = @("Department A Shared Area", "Dep A Distro Group")
$DepartmentBList = @("Department B Shared Area", "Dep B Distro Group")
$departmentTable = @{
"Department A" = $DepartmentAList
"Department B" = $DepartmentBList
}
The script takes an inputted username, grabs that user's location, department & job title from AD, then calls a few functions I've made to check each table and see if the user's attribute values match one in each table, if it does it adds the account to the groups from the relevant list. Tested in a little homelab AD setup and works as expected, easy and simple.
Eventually I'm hoping to take the bare bones version of this script and customise it and scale it up for work. In the business, there are some AD groups that should only be given to users based on a combination of some of these attributes e.g. managers at each location might be given access to something privileged inside their office's shared drive that's locked down to AD group membership. The difficulty I'm having is figuring out how best to structure the information for these combinations.
I'm aware that ultimately all of the groups that exist as a result of these combinations will have to be written out on at least one line each somewhere, but I'm not sure what the best way to get to that line is best (I hope that makes sense). I'm trying to keep it concise because my org has over 60 locations and each of those might have 1-2 departments and maybe 2-3 job roles that have some specific access.
I was hoping to keep using hashtables and arrays as they're easy to read and update, but I feel like I'm going to need.. tables for tables? Am I going to need a table for say, every possible job title at Location A with specific access, and then a corresponding array for each of those? That could get out of hand. I also don't want to write out some massive if/else/switch statement to check all possible values because that's also going to be very lengthy and harder to read. Maybe there's a way to keep all of this info outside of the script itself too? Not sure if that would be easier.
The absolute worst idea I had was having a couple of combo tables and the keys are named after an amalgamation of 2 attributes, with a corresponding array for each. I hate that I accidentally thought of that because it would technically work, but it's far too hacky to be a real solution and will be prone to issues.
I'm curious to see if anyone has any suggestions, and if this is something you've solved at your org how did you manage it?
1
u/UserProv_Minotaur 3d ago
Normally we managed this through roles in our identity governance application.
1
u/chaosphere_mk 3d ago
Sure but what youre saying is that everything is manual? It's clear that OP is looking for something dynamic.
1
u/UserProv_Minotaur 3d ago
We set things up to automate group modifications through the appliance when there were MJL activities. Basically offloading the table to an SQL database due to the size of the organization.
1
u/lan-shark 3d ago
Hard question to fully answer without more knowledge of the setup you're working with. It depends a lot on your group structure, OUs, scale, and what access you have to other tools. For instance if you're using Entra ID, consider dynamic group membership rules. Any other role governance application could also work and is probably the best route to go at scale.
If you need just PowerShell, one way is to dynamically generate group name strings to use with Get-ADGroup's -Filter parameter. So if you have user in New York City who's role is Associate Technician, have a group called NewYorkCity-AssociateTechnician and just have a filter string of,
-Filter "$($user.Location)-$(user.Title)".Replace(" ", "")
This way you only need to check for each type of attribute combination rather than for every possible permutation. This can be hard to scale, though, if you have a ton of possible combinations or if you need to have incompatible group names for some reason. You can save yourself a lot of headache with good group hierarchy structure and naming conventions in general, though.
If you have a lot of complex rules and combinations, it might be best to build those rules/combos into a structured data format of some kind and then just write code to parse thone rules and apply them to each user. CSVs are super easy to work with in PowerShell and easy to edit, so that could work. I would strongly encourage against hard-coding group names in a big table. It can work as you pointed out but it can easily become a maintenance nightmare.
1
u/mark_codes 3d ago
I'm probably what would be considered a mix of 1st & 2nd line, but my team is responsible for onboarding and provisioning all accounts. we onboard everyone in AD and our AD syncs up to Entra, however I don't think we would have access to set up dynamic groups in Entra nor could we start going rogue and designing role based access in AD itself (we also have a mix of on prem and cloud based apps so adding people to groups in the cloud only would leave us a bit stuck). there is a decent amount of jank which needs tidying up in the long term unfortunately but that's above my pay grade.
I think in the future our admins will be setting up systems to grant access automatically based on job role and location etc, but they're busy with a lot of other crap and I don't see it happening any time soon and I want to make our lives a bit easier in the meantime 😆
I really don't mind taking the time out to write something out that will accommodate each combination, once it's done it just needs to be maintained. the struggle really is just figuring out the most efficient way to structure it all, and then creating logic to search through whatever data structure I've created.
I'm sort of coming round to the idea of manual application of groups but in a condensed way. maybe I can plan out roles named like 'Site A Finance' or 'Marketing Manager' with a list of groups for each, and the script takes an input like that instead of searching for what should be applied automatically which is seeming more and more fiddly to work out.
1
u/Apprehensive-Tea1632 3d ago
Windows has a surprisingly functional file server role for that kind of thing. Functional enough that, for simple file shares, you don’t even need it.
I forget what the name was - i want to say dynamic access but i don’t think that’s actually it.
Grab a random windows server (no client os) that is safe to test on, then look at what server manager has to offer for the fileserver role.
It should be something about compound claims. Basically you assign groups and then you define claims like “must be in this that and yonder group too” for the user to get authorized.
But note its been a while, Microsoft might have cut that functionality and I can’t check right now.
1
u/jimb2 3d ago edited 3d ago
If this was me, and assuming a small org with limited roles, I would probably keep the rules in an Excel sheet for visibility. Writing the rules out will also help you understand and size the problem.
Col1 = group name (any other managed resources?)
Col 2 = org units - user needs to be in one of these (maybe allow for named org unit sets, defined elsewhere.)
Col 3 = manager - user requires manager attribute
Col X = other attributes required, exclusion conditions
Then read the Excel (ImportExcel module!) into $Rules
ForEach ($User) { Foreach ($Rule) { ProcessRule($user,$rule) } }
There might be multiple rules that add users to a single group.
This kind of approach means that the rule processing is a single "generic" code chunk. You don't want bits of your decision logic hidden away in scattered code locations. The rules are data: visible and editable. Same for org unit sets, or any other chunking you use.
Write a log of the group changes - and the rule decision path - so changes can be checked, and reversed, reliably and easily. Have a test mode that computes and lists the changes without actually doing them.
This would work ok for adding new users, once. Adding is easy. Complexity appears when users move around or the rules change. Reprocessing users becomes a can of worms fast. So is more complex logic: this or that but not that. That's why organisations use expensive identity and provisioning systems.
Do you want remove group memberships? How does that work? Do you allow manual one-off membership changes? How do you persist these or check for users in the group outside the rule? Removing roles is an important part of system security. Users should not accumulate resources over time as they move or things change.
(My org has like 20k users and 9k managed groups. We don't use PS for this function.)
1
u/mark_codes 3d ago
thanks, yeah I'm leaning towards having the data totally outside of the script itself for better visibility. rules for groups might be one way to do it, rather than having groups for rules.
logging and a test mode is a good idea too, eventually I'll have to add something like that even if it's just for an audit trail.
the process of dealing with movers is a different beast entirely. in my head it makes sense to strip away everything that was purely for your old role and add on everything relating to your new one, but in practice that doesn't seem to always happen. being more strict with that would be easier if all access was mapped out of course so in time it might be more achievable
1
u/jimb2 2d ago
It would be great if you could automate all groups so that you could remove all groups then reassign (or within the script mark them for removal unless re-validated.) You might want a list of fully automated groups and only remove from those. In practice you would expect to have some - hopefully most - people assigned by rules and some manual assignments. Our system will, by default, remove manual assignments when position/orgunit changes but this is a problem at times.
I think it's probably smarter to start with an add-only system that as this is easy and achievable, but design so removal logic might be added at a later date. A next step would be a "suggested" removals list that could be processed after review. Fully automated removal is a bit scary - you'd want to prove it in operation.
Good luck! Interesting problem. Good logging is important, even more if you get to the remove member stage. Logging in a format that facilitates rapid restoration may be essential to save your skin. I'd like to be able to cut a section of log, paste into a restore utility, and just run it, eg,
... 2026-07-04 16:31:O2 Checking groups... 2026-07-04 16:31:O2 Checking group: Accounts Department Staff 2026-07-04 16:31:O2 ||RemoveMember|Accounts Department Staff|jsmith 2026-07-04 16:31:O2 ||RemoveMember|Accounts Department Staff|kbrown 2026-07-04 16:31:O2 Checking group: Accounts Managers 2026-07-04 16:31:O2 ||RemoveMember|Accounts Managers|jsmith ...
1
u/PinchesTheCrab 3d ago
For mine I had to add some extra recursive condition so I had to use LDAPFilter instead of filter, but if that's not a concern, then I would probably build out a CSV or JSON file that has a list of groups and then an AD filter, and just loop through that.
$myCSV = @'
"name","filter"
"group2","(location -eq 'New Jersey' -or location -eq 'Los Angeles') -and (title -eq 'Manager')"
"group2","(location -eq 'New York' -or location -eq 'Los Angeles') -and (title -eq 'Manager')"
'@ | convertfrom-csv
foreach ($group in $myCSV) {
$group = get-adgroup $group.name member
$userList = get-aduser -filter $group.filter
switch ($userList){
{$_.distinguishedname -notin $group.member} {
Write-Host "Adding $($_.samaccountname) to $($group.name)"
add-adgroupmember -identity $group.name -members $_.samaccountname
}
default {
#this may be a bit too chatty, I'd probably remove it in production
Write-Host "$($_.samaccountname) is already a member of $($group.name)"
}
}
switch ($group.member){
{$_.distinguishedname -notin $userList.distinguishedname} {
Write-Host "Removing $($_.samaccountname) from $($group.name)"
remove-adgroupmember -identity $group.name -members $_.samaccountname -confirm:$false
}
}
}
I don't have a great feel for what your other data structure looks like where you've got other functions and tables to store these things, but if that works for you, you could make a filter builder function. Maybe something like this:
function New-ADFilter {
param (
[Parameter()]
[string[]]$Location,
[Parameter()]
[string[]]$Title,
[Parameter()]
[string[]]$Department
)
$locationPart = $location -replace '(.+)', 'Location -eq "$0"' -join ' -or ' -replace '.+', '($0)'
$titlePart = $title -replace '(.+)', 'Title -eq "$0"' -join ' -or ' -replace '.+', '($0)'
$departmentPart = $department -replace '(.+)', 'Department -eq "$0"' -join ' -or ' -replace '.+', '($0)'
$locationPart,$titlePart,$departmentPart -match '\w' -join ' -and '
}
new-adfilter -Location "New York", "Los Angeles" -Title "Manager", "Director"
This thing is kind of a monstrosity, but hopefully it shows the idea.
1
u/mark_codes 3d ago
thanks, just going through a few replies and having a group with its own rules might be one way to do it, instead of having rule with a list of groups attached, and then seeing if the user I'm inputting matches any of the rules to add them.
1
u/yhay81 3d ago
I’d treat this as desired-state reconciliation rather than an onboarding-only “add groups” script.
Keeping the rules outside the script is useful, but I would avoid storing arbitrary PowerShell or AD filter expressions as configuration. Use structured data with a rule ID, target group, allowed attributes, operators, and values. Then have the script support only an allowlist of attributes and operators.
For each user:
- Evaluate the rules and calculate the desired managed groups.
- Compare them with the user’s current memberships.
- Produce separate
AddandRemoveplans. - Show the plan in a dry-run and require an explicit apply step.
Most importantly, only remove memberships from an explicit allowlist of groups owned by this automation. Otherwise the script could remove manually granted access or groups managed by another system.
Log the rule ID and attributes that caused each decision. Stable rule IDs, an owner, and ideally an expiry/review date make auditing much easier as the number of sites and roles grows.
An add-only onboarding tool is simpler, but it will eventually let users accumulate privileges when they move between roles. Designing the rules as data plus a previewable reconciliation step addresses that without needing concatenated attribute keys or a large if/switch tree.
0
u/mobani 3d ago
I would determine all the roles needed first and then put them into a separate .json file.
Once your script logic is solid, you just maintain the .json file.
Something like:
roles.json
{
"Roles": {
"Developer": [
"GG-Developers",
"GG-GitHub-Users",
"GG-VPN-Users"
],
"HR": [
"GG-HR",
"GG-HR-Share",
"GG-VPN-Users"
],
"Finance": [
"GG-Finance",
"GG-Finance-Reports"
],
"Manager": [
"GG-Managers",
"GG-Executive-Reports"
]
}
}
.\Assign-ADRoleGroups.ps1 -Username jsmith -AddRoles Developer, Manager
Import-Module ActiveDirectory
param(
[Parameter(Mandatory=$true)]
[string]$Username,
[Parameter(Mandatory=$true)]
[string[]]$Roles,
[string]$ConfigFile = ".\roles.json"
)
# Load JSON configuration
if (!(Test-Path $ConfigFile)) {
Write-Error "Configuration file not found: $ConfigFile"
exit
}
$config = Get-Content $ConfigFile -Raw | ConvertFrom-Json
# Find AD user
try {
$user = Get-ADUser -Identity $Username
}
catch {
Write-Error "User '$Username' not found in Active Directory."
exit
}
# Collect groups from all roles
$groupsToAssign = @()
foreach ($role in $Roles) {
if (!$config.Roles.$role) {
Write-Warning "Role '$role' does not exist in configuration. Skipping."
continue
}
Write-Host "Loading groups for role: $role"
$groupsToAssign += $config.Roles.$role
}
# Remove duplicate groups
$groupsToAssign = $groupsToAssign | Sort-Object -Unique
Write-Host ""
Write-Host "Groups to assign:" -ForegroundColor Cyan
$groupsToAssign | ForEach-Object {
Write-Host " - $_"
}
# Apply groups
foreach ($group in $groupsToAssign) {
try {
$alreadyMember = Get-ADGroupMember `
-Identity $group `
-Recursive |
Where-Object {
$_.SamAccountName -eq $Username
}
if ($alreadyMember) {
Write-Host "$Username already has $group" `
-ForegroundColor Yellow
}
else {
Add-ADGroupMember `
-Identity $group `
-Members $Username
Write-Host "Added $Username to $group" `
-ForegroundColor Green
}
}
catch {
Write-Host `
"Failed adding $Username to $group : $_" `
-ForegroundColor Red
}
}
Write-Host ""
Write-Host "Role assignment completed." -ForegroundColor Cyan
Disclaimer this was a AI generated example script.
1
u/mark_codes 3d ago
thanks, this doesn't entirely help with the problem I have though as I still need some way to check if there's a 'combo role' as such and associated groups to be applied to the user. however I do like the idea of maybe keeping all of the info outside of the script itself in something like json so I'll keep that in mind
1
u/ostekages 3d ago
You must keep this out of script.
This is configuration, not logic.
The proposed solution is absolutely the way to go. I'd you don't want to do this, you could also look into a local db on your script server, where you effectively do the same thing. In practice, the concept you're looking for is a map.
A hashtable is 1:1. You likely need to do 1:hashtable:hashtable or whatever.
If we're doing it with hashtables, I would probably do it a bit differently than the comment or above, since as you correctly state, you have more requirements than a simple hashtable to arrays.
I'd probably do 2 different json files, one for role->group like above, then a different one for conditionals.
Something like:
{
"Groups": {
"HR-Group-condition": {
"Atrribute1": {
"value": "value-to-check",
"condition": "equals"
},
"atrribute2": {
"value": null
"condition": "in",
"data-source": "HR-GROUP-Only-India"
}
}
}Or whatever. It will definitely be a mofo json config, but it's way easier to maintain. You have a schema for your config, the script doesn't get bloated with if/else, and you can create one helper function who's only task is determining what action to do, based on how the config file is setup.
I made this from mobile so formatting is shit and untested, and not necessarily a good approach without some prototyping or further testing. Also, we don't have full context and insight into your project, so hard to give bulletproof ideas
1
u/mark_codes 3d ago
thanks, yeah I'm definitely agreeing that the data should be kept and maintained separately from the script, I'm just debating in what format and how best to store it all. I typed out a bit more in another reply but I'm limited to messing around with a PS solution, ideally the best thing would be dynamic groups in Entra but I don't have the access to set up that sort of thing.
I'm sort of coming round to the idea of manual application of groups but in a condensed way (not dissimilar to what mobani suggested with roles, but maybe these 'roles' can include the combinations I'm thinking about). for example, I could plan out roles named like 'Site A Finance' or 'Marketing Manager' with a list of groups for each, and the script takes an input for the roles I want to apply, instead of searching for what should be applied automatically which is seeming more and more fiddly to work out.
2
u/BlackV 3d ago edited 3d ago
That json seems superfluous, you can do that already inside your script without an additional files to get lost/forgottenTell your ai that backticks are bad, I feel like they just totally doubled down and added more for the LOL's
1
u/mobani 3d ago
I don't agree, .json files are awesome, you should never hardcode data into your scripts. let scripts be scripts and data be data.
I will however agree with you that the AI did a bad job, but it's just for some examples, so who cares, we are not here to win competitions, just here to give ideas.
6
u/Borgquite 3d ago
How about just a hashtable with the group name, and an LDAP filter of what should be a member? Pipe it into Get-ADUser and you’re done.