Skip to main content
cancel
Showing results for 
Search instead for 
Did you mean: 

Microsoft is giving away 50,000 FREE Microsoft Certification exam vouchers. Get Fabric certified for FREE! Learn more

Reply
Moof
Frequent Visitor

Active Directory hierarchy - getting all users inside a group including child groups

Hi,

 

I've been playing around with Active Directory as a data source on PowerBI for a while, trying to make reports.

 

One report I'm interested in is "Users in the Administrators Group"

 

My Administrators group has a number of users in it, as well as a Group - Domain Admins.

 

Domain Admins itself has a group in it too - Security administrators.

 

I'm trying to navigate the tree somehow either in M or DAX so that I can get a list of "In the Administrators Group, all the users are x, y, and z" where x is in Administrators, y is in Domain Admins, and z is in Security Administrators.

 

I've been thinking about seeing if I can get a list of "this group is a member of this group" (which I can do using top.memberOf) but I can't seem to work out how to navigate from Security Admins to Administrators, so I can see that Security Admins is a member of Domain admins and Administrators.

 

If I could do that, from there I could just say that since user z is in Security Administrators, it's also in Domain Administrators and Administrators. 

 

I'm specifically trying to find a generic solution here, rather than hard coding my tree into the programme.

 

Am I barking up the wrong tree? How do I do this sort of recursive query in M/DAX?

 

6 REPLIES 6
Madalina2801
Advocate I
Advocate I

Hello,

 

Are there any updates here? Do you know if there is a way to expand all levels of nested groups recursively?

 

Thanks!

dtjdtj
Advocate I
Advocate I

Has anyone found a solution here? I'm also interested in expanding all levels of nested groups recursively. 
Thank you in advance!

v-yuezhe-msft
Microsoft Employee
Microsoft Employee

@Moof,

Please help to post sample data of your tables and post expected result  based on sample data here.

Regards,
Lydia

Community Support Team _ Lydia Zhang
If this post helps, then please consider Accept it as the solution to help the other members find it more quickly.

Or, placing it a different way...

 

I have a hierachy of groups in ActiveDirectory, and a bunch of users that belong to those groups.

 

Security Administrators is a member of Domain Admins which is a member of Administrators. Each of these groups has users in it. I want to filter by group Administrators, and get users from all the subgroups.

 

Essentially, using sample date, the result I'm looking for is:

 

Sample result.png

 

The code I'm using to get there is as follows:

 

let
    Source = ActiveDirectory.Domains("lapsang.example"),
    lapsang.example = Source{[Domain="lapsang.example"]}[#"Object Categories"],
    user1 = lapsang.example{[Category="user"]}[Objects],
    #"Expanded top" = Table.ExpandRecordColumn(user1, "top", {"cn", "memberOf"}, {"top.cn", "top.memberOf"}),
    #"Removed Other Columns" = Table.SelectColumns(#"Expanded top",{"top.cn", "top.memberOf", "distinguishedName"}),
    #"Expanded top.memberOf" = Table.ExpandListColumn(#"Removed Other Columns", "top.memberOf"),
    #"Expanded top.memberOf1" = Table.ExpandRecordColumn(#"Expanded top.memberOf", "top.memberOf", {"distinguishedName", "memberOf"}, {"top.memberOf.distinguishedName", "top.memberOf.memberOf"}),
    #"Reordered Columns1" = Table.ReorderColumns(#"Expanded top.memberOf1",{"top.cn", "distinguishedName", "top.memberOf.distinguishedName", "top.memberOf.memberOf"}),
    #"Expanded top.memberOf.memberOf" = Table.ExpandListColumn(#"Reordered Columns1", "top.memberOf.memberOf"),
    #"Expanded top.memberOf.memberOf1" = Table.ExpandRecordColumn(#"Expanded top.memberOf.memberOf", "top.memberOf.memberOf", {"distinguishedName", "memberOf"}, {"top.memberOf.memberOf.distinguishedName", "top.memberOf.memberOf.memberOf"}),
    #"Expanded top.memberOf.memberOf.memberOf" = Table.ExpandListColumn(#"Expanded top.memberOf.memberOf1", "top.memberOf.memberOf.memberOf"),
    #"Expanded top.memberOf.memberOf.memberOf1" = Table.ExpandRecordColumn(#"Expanded top.memberOf.memberOf.memberOf", "top.memberOf.memberOf.memberOf", {"distinguishedName", "memberOf"}, {"top.memberOf.memberOf.memberOf.distinguishedName", "top.memberOf.memberOf.memberOf.memberOf"}),
    #"Unpivoted Columns" = Table.UnpivotOtherColumns(#"Expanded top.memberOf.memberOf.memberOf1", {"top.cn", "distinguishedName", "top.memberOf.memberOf.memberOf.memberOf"}, "Attribute", "Value"),
    #"Grouped Rows" = Table.Group(#"Unpivoted Columns", {"top.cn", "distinguishedName", "Value"}, {{"Count", each Table.RowCount(_), type number}}),
    #"Renamed Columns" = Table.RenameColumns(#"Grouped Rows",{{"Value", "InGroup"}}),
    #"Removed Columns" = Table.RemoveColumns(#"Renamed Columns",{"Count"})
in
    #"Removed Columns"

Now, as you can see, I've used Table.ExpandListColumn, and Table.ExpandRecordColumn several times there, and I can't guarantee that if I query a different AD that I'll have managed to get all the levels of expanding. 

 

Is there a way to recurse through the table, such that it does that as many times as neccesary, and then leaves an unpivoted result?

Hi! I have the same question. Do you have some update?

OK, it's taken me a little while to sort out a test environment.

 

I have set up a fresh AD domain. The Built-in Administrators group has the following:

 

Administrators Group.png

 

And then I populated the hierarchy as follows:

Domain Admins GroupDomain Admins GroupSecurity Administrators GroupSecurity Administrators Group

 

As you can see, a fairly simple hierarchy.

 

I am trying to set up an audit screen that shows the members of a group, including any sub groups that are in the group. So if I were to filter by the group Administrators, I get the following expected result for users:

 

  • lapsangadmin
  • Siuan Sanche (admin)
  • Lean Sharif (admin)

 

So now I enter Power BI and use the AD connector to Users. This gives me a rather difficult to deal with table:

 

Unedited Users QueryUnedited Users Query

When I filter it a bit, and expand the "top" column I get:

Users first filter.png

 

Again, I expand those lists into separate rows, and expand the records, and I get...

 

Users expanded.png

 

 

The code I use to get there is:

let
    Source = ActiveDirectory.Domains("lapsang.example"),
    lapsang.example = Source{[Domain="lapsang.example"]}[#"Object Categories"],
    user1 = lapsang.example{[Category="user"]}[Objects],
    #"Expanded top" = Table.ExpandRecordColumn(user1, "top", {"cn", "memberOf"}, {"top.cn", "top.memberOf"}),
    #"Removed Other Columns" = Table.SelectColumns(#"Expanded top",{"top.cn", "top.memberOf", "distinguishedName"}),
    #"Expanded top.memberOf" = Table.ExpandListColumn(#"Removed Other Columns", "top.memberOf"),
    #"Expanded top.memberOf1" = Table.ExpandRecordColumn(#"Expanded top.memberOf", "top.memberOf", {"distinguishedName"}, {"top.memberOf.distinguishedName"}),
    #"Reordered Columns" = Table.ReorderColumns(#"Expanded top.memberOf1",{"top.cn", "distinguishedName", "top.memberOf.distinguishedName"})
in
    #"Reordered Columns"

 

with this, I can tell that lapsangadmin is in Administrators and Domain Admins, that Siuan Sanche (admin) is in Domain Admins and that Leane Sharif (admin) is in Security Admins.

 

But I can't filter by Administrators and get all three.

 

So I start to look at the group object instead, which is an equally unappetising initial query:

 

Groups unfiltered.png

 

In top, there is again a "memberOf" column, which has a list of other records:

 

Groups first filter.png

 

And again, expanding that gives me:

 

Groups expanded.png

 

The code I use to get here is:

 

let
    Source = ActiveDirectory.Domains("lapsang.example"),
    lapsang.example = Source{[Domain="lapsang.example"]}[#"Object Categories"],
    user1 = lapsang.example{[Category="user"]}[Objects],
    #"Expanded top" = Table.ExpandRecordColumn(user1, "top", {"cn", "memberOf"}, {"top.cn", "top.memberOf"}),
    #"Removed Other Columns" = Table.SelectColumns(#"Expanded top",{"top.cn", "top.memberOf", "distinguishedName"}),
    #"Expanded top.memberOf" = Table.ExpandListColumn(#"Removed Other Columns", "top.memberOf"),
    #"Expanded top.memberOf1" = Table.ExpandRecordColumn(#"Expanded top.memberOf", "top.memberOf", {"distinguishedName"}, {"top.memberOf.distinguishedName"}),
    #"Reordered Columns" = Table.ReorderColumns(#"Expanded top.memberOf1",{"top.cn", "distinguishedName", "top.memberOf.distinguishedName"})
in
    #"Reordered Columns"

So this is where I'm stuck. 

 

Eventually, I could just keep expanding memberOf until I run out of things, and then unpivot the resulting distinguishedName columns into one. The thing is, I'm trying to make this a generic solution that doesn't need to know how many times to expand, because whilst in this test case, I only have a simple hierarchy like this, in some of the Active Directories I deal with this hierarchy may go up to 6 or 8 groups deep.

 

So I suppose I'm asking: how do I do this sort of recursion in M, such that I can get a table of all the parents a group may have, withough needing to do it a specific number of times?

 

I suppose I'm trying to get a table like this:

 

GroupContained in
AdministratorsAdministrators
Security AdministratorsSecurity Administrators
Security AdministratorsDomain Admins
Security AdministratorsAdministrators
Domain AdminsDomain Admins
Domain AdminsAdministrators

 ...and so on

 

 From there, I can link my users to that Group Column, and then filter by a summary of the Contained In Column.

 

Any hints gratefully appreciated...

Helpful resources

Announcements
March PBI video - carousel

Power BI Monthly Update - March 2025

Check out the March 2025 Power BI update to learn about new features.

March2025 Carousel

Fabric Community Update - March 2025

Find out what's new and trending in the Fabric community.

"); $(".slidesjs-pagination" ).prependTo(".pagination_sec"); $(".slidesjs-pagination" ).append("
"); $(".slidesjs-play.slidesjs-navigation").appendTo(".playpause_sec"); $(".slidesjs-stop.slidesjs-navigation").appendTo(".playpause_sec"); $(".slidesjs-pagination" ).append(""); $(".slidesjs-pagination" ).append(""); } catch(e){ } /* End: This code is added by iTalent as part of iTrack COMPL-455 */ $(".slidesjs-previous.slidesjs-navigation").attr('tabindex', '0'); $(".slidesjs-next.slidesjs-navigation").attr('tabindex', '0'); /* start: This code is added by iTalent as part of iTrack 1859082 */ $('.slidesjs-play.slidesjs-navigation').attr('id','playtitle'); $('.slidesjs-stop.slidesjs-navigation').attr('id','stoptitle'); $('.slidesjs-play.slidesjs-navigation').attr('role','tab'); $('.slidesjs-stop.slidesjs-navigation').attr('role','tab'); $('.slidesjs-play.slidesjs-navigation').attr('aria-describedby','tip1'); $('.slidesjs-stop.slidesjs-navigation').attr('aria-describedby','tip2'); /* End: This code is added by iTalent as part of iTrack 1859082 */ }); $(document).ready(function() { if($("#slides .item").length < 2 ) { /* Fixing Single Slide click issue (commented following code)*/ // $(".item").css("left","0px"); $(".item.slidesjs-slide").attr('style', 'left:0px !important'); $(".slidesjs-stop.slidesjs-navigation").trigger('click'); $(".slidesjs-previous").css("display", "none"); $(".slidesjs-next").css("display", "none"); } var items_length = $(".item.slidesjs-slide").length; $(".slidesjs-pagination-item > button").attr("aria-setsize",items_length); $(".slidesjs-next, .slidesjs-pagination-item button").attr("tabindex","-1"); $(".slidesjs-pagination-item button").attr("role", "tab"); $(".slidesjs-previous").attr("tabindex","-1"); $(".slidesjs-next").attr("aria-hidden","true"); $(".slidesjs-previous").attr("aria-hidden","true"); $(".slidesjs-next").attr("aria-label","Next"); $(".slidesjs-previous").attr("aria-label","Previous"); //$(".slidesjs-stop.slidesjs-navigation").attr("role","button"); //$(".slidesjs-play.slidesjs-navigation").attr("role","button"); $(".slidesjs-pagination").attr("role","tablist").attr("aria-busy","true"); $("li.slidesjs-pagination-item").attr("role","list"); $(".item.slidesjs-slide").attr("tabindex","-1"); $(".item.slidesjs-slide").attr("aria-label","item"); /*$(".slidesjs-stop.slidesjs-navigation").on('click', function() { var itemNumber = parseInt($('.slidesjs-pagination-item > a.active').attr('data-slidesjs-item')); $($('.item.slidesjs-slide')[itemNumber]).find('.c-call-to-action').attr('tabindex', '0'); });*/ $(".slidesjs-stop.slidesjs-navigation, .slidesjs-pagination-item > button").on('click keydown', function() { $.each($('.item.slidesjs-slide'),function(i,el){ $(el).find('.c-call-to-action').attr('tabindex', '-1'); }); var itemNumber = parseInt($('.slidesjs-pagination-item > button.active').attr('data-slidesjs-item')); $($('.item.slidesjs-slide')[itemNumber]).find('.c-call-to-action').attr('tabindex', '0'); }); $(".slidesjs-play.slidesjs-navigation").on('click', function() { $.each($('.item.slidesjs-slide'),function(i,el){ $(el).find('.c-call-to-action').attr('tabindex', '-1'); }); }); $(".slidesjs-pagination-item button").keyup(function(e){ var keyCode = e.keyCode || e.which; if (keyCode == 9) { e.preventDefault(); $(".slidesjs-stop.slidesjs-navigation").trigger('click').blur(); $("button.active").focus(); } }); $(".slidesjs-play").on("click",function (event) { if (event.handleObj.type === "click") { $(".slidesjs-stop").focus(); } else if(event.handleObj.type === "keydown"){ if (event.which === 13 && $(event.target).hasClass("slidesjs-play")) { $(".slidesjs-stop").focus(); } } }); $(".slidesjs-stop").on("click",function (event) { if (event.handleObj.type === "click") { $(".slidesjs-play").focus(); } else if(event.handleObj.type === "keydown"){ if (event.which === 13 && $(event.target).hasClass("slidesjs-stop")) { $(".slidesjs-play").focus(); } } }); $(".slidesjs-pagination-item").keydown(function(e){ switch (e.which){ case 37: //left arrow key $(".slidesjs-previous.slidesjs-navigation").trigger('click'); e.preventDefault(); break; case 39: //right arrow key $(".slidesjs-next.slidesjs-navigation").trigger('click'); e.preventDefault(); break; default: return; } $(".slidesjs-pagination-item button.active").focus(); }); }); // Start This code is added by iTalent as part of iTrack 1859082 $(document).ready(function(){ $("#tip1").attr("aria-hidden","true").addClass("hidden"); $("#tip2").attr("aria-hidden","true").addClass("hidden"); $(".slidesjs-stop.slidesjs-navigation, .slidesjs-play.slidesjs-navigation").attr('title', ''); $("a#playtitle").focus(function(){ $("#tip1").attr("aria-hidden","false").removeClass("hidden"); }); $("a#playtitle").mouseover(function(){ $("#tip1").attr("aria-hidden","false").removeClass("hidden"); }); $("a#playtitle").blur(function(){ $("#tip1").attr("aria-hidden","true").addClass("hidden"); }); $("a#playtitle").mouseleave(function(){ $("#tip1").attr("aria-hidden","true").addClass("hidden"); }); $("a#play").keydown(function(ev){ if (ev.which ==27) { $("#tip1").attr("aria-hidden","true").addClass("hidden"); ev.preventDefault(); return false; } }); $("a#stoptitle").focus(function(){ $("#tip2").attr("aria-hidden","false").removeClass("hidden"); }); $("a#stoptitle").mouseover(function(){ $("#tip2").attr("aria-hidden","false").removeClass("hidden"); }); $("a#stoptitle").blur(function(){ $("#tip2").attr("aria-hidden","true").addClass("hidden"); }); $("a#stoptitle").mouseleave(function(){ $("#tip2").attr("aria-hidden","true").addClass("hidden"); }); $("a#stoptitle").keydown(function(ev){ if (ev.which ==27) { $("#tip2").attr("aria-hidden","true").addClass("hidden"); ev.preventDefault(); return false; } }); }); // End This code is added by iTalent as part of iTrack 1859082