Merge pull request #107 from Microsoft/users/jmarks/EnumMembers
Adding sample for fully enumerating users of a group
This commit is contained in:
Коммит
2c25e016de
|
@ -42,7 +42,7 @@
|
|||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.16.0.14" newVersion="3.16.0.14" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.13.8.999" newVersion="3.13.8.999" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.IdentityModel.Tokens.Jwt" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
|
@ -50,7 +50,7 @@
|
|||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="Microsoft.IdentityModel.Clients.ActiveDirectory.Platform" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.16.0.14" newVersion="3.16.0.14" />
|
||||
<bindingRedirect oldVersion="0.0.0.0-3.13.8.999" newVersion="3.13.8.999" />
|
||||
</dependentAssembly>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" />
|
||||
|
|
|
@ -33,11 +33,11 @@
|
|||
<WarningLevel>4</WarningLevel>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory, Version=3.13.5.907, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.5\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll</HintPath>
|
||||
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory, Version=3.13.8.999, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory.Platform, Version=3.13.5.907, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.5\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll</HintPath>
|
||||
<Reference Include="Microsoft.IdentityModel.Clients.ActiveDirectory.Platform, Version=3.13.8.999, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll</HintPath>
|
||||
</Reference>
|
||||
<Reference Include="Microsoft.ServiceBus, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
|
||||
<HintPath>packages\WindowsAzure.ServiceBus.3.3.2\lib\net45-full\Microsoft.ServiceBus.dll</HintPath>
|
||||
|
@ -115,10 +115,13 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="Program.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="Samples\EnumerateMembersOfGroups.cs" />
|
||||
<Compile Include="Samples\EnumerateUsers.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="App.config">
|
||||
<SubType>Designer</SubType>
|
||||
</None>
|
||||
<None Include="packages.config" />
|
||||
</ItemGroup>
|
||||
<ItemGroup />
|
||||
|
|
|
@ -13,11 +13,11 @@ namespace GraphQuickStarts
|
|||
return 0;
|
||||
}
|
||||
|
||||
string connectionUrl, token = "";
|
||||
string connectionUrl, token, groupName, clientId, redirectURL = "";
|
||||
|
||||
try
|
||||
{
|
||||
CheckArguments(args, out connectionUrl, out token);
|
||||
CheckArguments(args, out connectionUrl, out token, out groupName, out clientId, out redirectURL);
|
||||
}
|
||||
catch (ArgumentException ex)
|
||||
{
|
||||
|
@ -38,7 +38,11 @@ namespace GraphQuickStarts
|
|||
//execute the client lib code. If you want to run the direct http calls then adjust (see below)
|
||||
objUsers.RunEnumerateUsersUsingClientLib();
|
||||
|
||||
objUsers = null;
|
||||
//instantiate objects & execute
|
||||
Samples.EnumerateMembersOfGroups objMembers = new Samples.EnumerateMembersOfGroups(connectionUrl, clientId, redirectURL);
|
||||
|
||||
//execute the client lib code. If you want to run the direct http calls then adjust (see below)
|
||||
objMembers.RunEnumerateMembersOfGroupsUsingClientLib(groupName);
|
||||
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
@ -61,16 +65,19 @@ namespace GraphQuickStarts
|
|||
Console.WriteLine("");
|
||||
Console.WriteLine("Arguments:");
|
||||
Console.WriteLine("");
|
||||
Console.WriteLine(" /url:fabrikam.vssps.visualstudio.com /token:personalaccesstoken");
|
||||
Console.WriteLine(" /url:http://fabrikam.vssps.visualstudio.com /token:personalaccesstoken /group:Developers /clientId:7e2fa445-a7c0-48b2-b1b2-be805e7a2fdf /redirectUrl:http://sampleUrl");
|
||||
Console.WriteLine("");
|
||||
|
||||
Console.ReadKey();
|
||||
}
|
||||
|
||||
private static void CheckArguments(string[] args, out string connectionUrl, out string token)
|
||||
private static void CheckArguments(string[] args, out string connectionUrl, out string token, out string groupName, out string clientId, out string redirectUrl)
|
||||
{
|
||||
connectionUrl = null;
|
||||
token = null;
|
||||
groupName = null;
|
||||
clientId = null;
|
||||
redirectUrl = null;
|
||||
|
||||
Dictionary<string, string> argsMap = new Dictionary<string, string>();
|
||||
foreach (var arg in args)
|
||||
|
@ -90,6 +97,18 @@ namespace GraphQuickStarts
|
|||
token = value;
|
||||
break;
|
||||
|
||||
case "group":
|
||||
groupName = value;
|
||||
break;
|
||||
|
||||
case "clientId":
|
||||
clientId = value;
|
||||
break;
|
||||
|
||||
case "redirectUrl":
|
||||
redirectUrl = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ArgumentException("Unknown argument", key);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,295 @@
|
|||
using Microsoft.IdentityModel.Clients.ActiveDirectory;
|
||||
using Microsoft.VisualStudio.Services.Common;
|
||||
using Microsoft.VisualStudio.Services.Graph;
|
||||
using Microsoft.VisualStudio.Services.Graph.Client;
|
||||
using Microsoft.VisualStudio.Services.OAuth;
|
||||
using Microsoft.VisualStudio.Services.WebApi;
|
||||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
|
||||
namespace GraphQuickStarts.Samples
|
||||
{
|
||||
class EnumerateMembersOfGroups
|
||||
{
|
||||
readonly string _uri;
|
||||
readonly Guid _clientId;
|
||||
readonly Uri _replyUrl;
|
||||
|
||||
internal const string VSTSResourceId = "499b84ac-1321-427f-aa17-267ca6975798"; //Constant value to target VSTS. Do not change
|
||||
internal const string GraphResourceId = "https://graph.microsoft.com"; //Constant value to target Microsoft Graph API. Do not change
|
||||
|
||||
/// <summary>
|
||||
/// Constructor. Manaully set values to match your account.
|
||||
/// </summary>
|
||||
public EnumerateMembersOfGroups()
|
||||
{
|
||||
_uri = "https://accountname.vssps.visualstudio.com";
|
||||
_clientId = new Guid("XXXXXXX -XXXX-XXXX-XXXX-XXXXXXXXXXXX");
|
||||
_replyUrl = new Uri("http://MyAppUrl");
|
||||
}
|
||||
|
||||
public EnumerateMembersOfGroups(string url, string clientId, string redirectURL)
|
||||
{
|
||||
_uri = url;
|
||||
_clientId = new Guid(clientId);
|
||||
_replyUrl = new Uri(redirectURL);
|
||||
}
|
||||
|
||||
public List<string> RunEnumerateMembersOfGroupsUsingClientLib(string groupDisplayName)
|
||||
{
|
||||
Uri uri = new Uri(_uri);
|
||||
AuthenticationContext ctx = GetAuthenticationContext(null);
|
||||
AuthenticationResult vstsAuthResult = ctx.AcquireTokenAsync(VSTSResourceId, _clientId.ToString(), _replyUrl, new PlatformParameters(PromptBehavior.Always)).Result;
|
||||
VssConnection vssConnection = new VssConnection(new Uri(_uri), new VssOAuthAccessTokenCredential(vstsAuthResult.AccessToken));
|
||||
|
||||
using (GraphHttpClient graphClient = vssConnection.GetClient<GraphHttpClient>())
|
||||
{
|
||||
// Get the VSTS group
|
||||
GraphGroup group = GetVSTSGroupByDisplayName(graphClient, groupDisplayName);
|
||||
|
||||
// Expand membership of the VSTS group to users and AAD Groups
|
||||
GroupMemberships groupMemberships = ExpandVSTSGroup(graphClient, group);
|
||||
|
||||
List<string> expandedUsers = new List<string>();
|
||||
foreach (GraphUser user in groupMemberships.Users)
|
||||
{
|
||||
expandedUsers.Add(user.PrincipalName);
|
||||
}
|
||||
|
||||
//exchange VSTS token for Microsoft graph token
|
||||
AuthenticationResult graphAuthResult = ctx.AcquireTokenAsync(GraphResourceId, _clientId.ToString(), _replyUrl, new PlatformParameters(PromptBehavior.Auto)).Result;
|
||||
|
||||
// Resolve all AAD Groups to users using Microsoft graph
|
||||
foreach (GraphGroup AADGroup in groupMemberships.AADGroups)
|
||||
{
|
||||
List<AadGroupMember> aadGroupUsers = ExpandAadGroups(graphAuthResult.AccessToken, AADGroup);
|
||||
foreach (AadGroupMember aadGroupUser in aadGroupUsers)
|
||||
{
|
||||
expandedUsers.Add(aadGroupUser.userPrincipalName);
|
||||
}
|
||||
}
|
||||
|
||||
return expandedUsers;
|
||||
}
|
||||
}
|
||||
|
||||
#region ADAL helpers
|
||||
private static AuthenticationContext GetAuthenticationContext(string tenant)
|
||||
{
|
||||
AuthenticationContext ctx = null;
|
||||
if (tenant != null)
|
||||
ctx = new AuthenticationContext("https://login.microsoftonline.com/" + tenant);
|
||||
else
|
||||
{
|
||||
ctx = new AuthenticationContext("https://login.windows.net/common");
|
||||
if (ctx.TokenCache.Count > 0)
|
||||
{
|
||||
string homeTenant = ctx.TokenCache.ReadItems().First().TenantId;
|
||||
ctx = new AuthenticationContext("https://login.microsoftonline.com/" + homeTenant);
|
||||
}
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region VSTS Graph helpers
|
||||
private static GraphGroup GetVSTSGroupByDisplayName(GraphHttpClient graphClient, string groupDisplayName)
|
||||
{
|
||||
PagedGraphGroups groups = graphClient.GetGroupsAsync().Result;
|
||||
|
||||
GraphGroup selectedGroup = null;
|
||||
foreach (var group in groups.GraphGroups)
|
||||
{
|
||||
if (group.DisplayName.Equals(groupDisplayName))
|
||||
{
|
||||
return selectedGroup = group;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static GroupMemberships ExpandVSTSGroup(GraphHttpClient graphClient, GraphGroup group)
|
||||
{
|
||||
GroupMemberships groupMemberships = new GroupMemberships();
|
||||
|
||||
// Convert all memberships into GraphSubjectLookupKeys
|
||||
List<GraphSubjectLookupKey> lookupKeys = new List<GraphSubjectLookupKey>();
|
||||
List<GraphMembership> memberships = graphClient.GetMembershipsAsync(group.Descriptor, Microsoft.VisualStudio.Services.Graph.GraphTraversalDirection.Down).Result;
|
||||
foreach (var membership in memberships)
|
||||
{
|
||||
lookupKeys.Add(new GraphSubjectLookupKey(membership.MemberDescriptor));
|
||||
}
|
||||
IReadOnlyDictionary<SubjectDescriptor, GraphSubject> subjectLookups = graphClient.LookupSubjectsAsync(new GraphSubjectLookup(lookupKeys)).Result;
|
||||
foreach (GraphSubject subject in subjectLookups.Values)
|
||||
{
|
||||
switch (subject.Descriptor.SubjectType)
|
||||
{
|
||||
//member is an AAD user
|
||||
case Constants.SubjectType.AadUser:
|
||||
groupMemberships.AddUser((GraphUser)subject);
|
||||
break;
|
||||
|
||||
//member is an MSA user
|
||||
case Constants.SubjectType.MsaUser:
|
||||
groupMemberships.AddUser((GraphUser)subject);
|
||||
break;
|
||||
|
||||
//member is a nested AAD group
|
||||
case Constants.SubjectType.AadGroup:
|
||||
groupMemberships.AddAADGroup((GraphGroup)subject);
|
||||
break;
|
||||
|
||||
//member is a nested VSTS group
|
||||
case Constants.SubjectType.VstsGroup:
|
||||
GroupMemberships subGroupMemberships = ExpandVSTSGroup(graphClient, (GraphGroup)subject);
|
||||
groupMemberships.Add(subGroupMemberships);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new Exception("Unknown SubjectType: " + subject.Descriptor.SubjectType);
|
||||
}
|
||||
}
|
||||
|
||||
return groupMemberships;
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Microsoft Graph helpers
|
||||
private static List<AadGroupMember> ExpandAadGroups(string accessToken, GraphGroup group)
|
||||
{
|
||||
//List of users in an AAD group
|
||||
List<AadGroupMember> aadUsers = new List<AadGroupMember>();
|
||||
|
||||
//Getting all members in all groups and nesteed groups
|
||||
List<AadGroupMember> members = new List<AadGroupMember>();
|
||||
members.AddRange(GetAADGroupMembers(accessToken, group.OriginId));
|
||||
while (members.Count != 0)
|
||||
{
|
||||
List<AadGroupMember> nestedGroups = new List<AadGroupMember>();
|
||||
foreach (var aadMember in members)
|
||||
{
|
||||
switch (aadMember.type)
|
||||
{
|
||||
//member is a user
|
||||
case "#microsoft.graph.user":
|
||||
aadUsers.Add(aadMember);
|
||||
break;
|
||||
//member is a nested AAD group
|
||||
case "#microsoft.graph.group":
|
||||
nestedGroups.AddRange(GetAADGroupMembers(accessToken, aadMember.id));
|
||||
break;
|
||||
default:
|
||||
throw new Exception("shouldn't be here");
|
||||
}
|
||||
}
|
||||
members.Clear();
|
||||
members.AddRange(nestedGroups);
|
||||
}
|
||||
return aadUsers;
|
||||
}
|
||||
|
||||
private static List<AadGroupMember> GetAADGroupMembers(string accessToken, string aadGroupId)
|
||||
{
|
||||
AuthenticationHeaderValue authHeader = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
// use the httpclient
|
||||
using (var client = new HttpClient())
|
||||
{
|
||||
client.BaseAddress = new Uri("https://graph.microsoft.com/");
|
||||
client.DefaultRequestHeaders.Accept.Clear();
|
||||
client.DefaultRequestHeaders.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "GraphGroupMembershipSample");
|
||||
client.DefaultRequestHeaders.Add("X-TFS-FedAuthRedirect", "Suppress");
|
||||
client.DefaultRequestHeaders.Authorization = authHeader;
|
||||
|
||||
// connect to the REST endpoint
|
||||
HttpResponseMessage response = client.GetAsync("v1.0/groups/" + aadGroupId + "/members").Result;
|
||||
|
||||
// check to see if we have a succesfull respond
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
string responseJsonStr = response.Content.ReadAsStringAsync().Result;
|
||||
AadGroupMembers groupMembers = JsonConvert.DeserializeObject<AadGroupMembers>(responseJsonStr);
|
||||
return groupMembers.members;
|
||||
}
|
||||
else if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception();
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public class GroupMemberships
|
||||
{
|
||||
public List<GraphUser> Users;
|
||||
public List<GraphGroup> AADGroups;
|
||||
|
||||
public GroupMemberships()
|
||||
{
|
||||
Users = new List<GraphUser>();
|
||||
AADGroups = new List<GraphGroup>();
|
||||
}
|
||||
|
||||
public void Add(GroupMemberships memberships)
|
||||
{
|
||||
this.Users.AddRange(memberships.Users);
|
||||
this.AADGroups.AddRange(memberships.AADGroups);
|
||||
}
|
||||
|
||||
public void AddUser(GraphUser user)
|
||||
{
|
||||
this.Users.Add(user);
|
||||
}
|
||||
|
||||
public void AddAADGroup(GraphGroup group)
|
||||
{
|
||||
this.AADGroups.Add(group);
|
||||
}
|
||||
}
|
||||
|
||||
#region JSON deserialization
|
||||
public class AadGroupMembers
|
||||
{
|
||||
[JsonProperty("@odata.context")]
|
||||
public string groupType { get; set; }
|
||||
[JsonProperty("value")]
|
||||
public List<AadGroupMember> members { get; set; }
|
||||
}
|
||||
public class AadGroupMember
|
||||
{
|
||||
[JsonProperty("@odata.type")]
|
||||
public string type { get; set; }
|
||||
[JsonProperty("id")]
|
||||
public string id { get; set; }
|
||||
[JsonProperty("businessPhones")]
|
||||
public List<string> businessPhones { get; set; }
|
||||
[JsonProperty("displayName")]
|
||||
public string displayName { get; set; }
|
||||
[JsonProperty("givenName")]
|
||||
public string givenName { get; set; }
|
||||
[JsonProperty("jobTitle")]
|
||||
public string jobTitle { get; set; }
|
||||
[JsonProperty("mail")]
|
||||
public string mail { get; set; }
|
||||
[JsonProperty("mobilePhone")]
|
||||
public string mobilePhone { get; set; }
|
||||
[JsonProperty("officeLocation")]
|
||||
public string officeLocation { get; set; }
|
||||
[JsonProperty("preferredLanguage")]
|
||||
public string preferredLanguage { get; set; }
|
||||
[JsonProperty("surname")]
|
||||
public string surname { get; set; }
|
||||
[JsonProperty("userPrincipalName")]
|
||||
public string userPrincipalName { get; set; }
|
||||
}
|
||||
#endregion
|
||||
}
|
|
@ -44,13 +44,16 @@ namespace GraphQuickStarts.Samples
|
|||
List<GraphUser> graphUsers = new List<GraphUser>(users.GraphUsers);
|
||||
|
||||
// If there are more than a page's worth of users, continue retrieving users from the server a page at a time
|
||||
if (users.ContinuationToken != null)
|
||||
{
|
||||
string continuationToken = users.ContinuationToken.FirstOrDefault();
|
||||
while (continuationToken != null)
|
||||
{
|
||||
users = graphClient.GetUsersAsync(continuationToken: continuationToken).Result;
|
||||
graphUsers.AddRange(users.GraphUsers);
|
||||
|
||||
if (users.ContinuationToken != null) {
|
||||
if (users.ContinuationToken != null)
|
||||
{
|
||||
continuationToken = users.ContinuationToken.FirstOrDefault();
|
||||
}
|
||||
else
|
||||
|
@ -58,6 +61,7 @@ namespace GraphQuickStarts.Samples
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return graphUsers;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<packages>
|
||||
<package id="Microsoft.AspNet.WebApi.Client" version="5.2.3" targetFramework="net452" />
|
||||
<package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="3.13.5" targetFramework="net452" />
|
||||
<package id="Microsoft.IdentityModel.Clients.ActiveDirectory" version="3.13.8" targetFramework="net452" />
|
||||
<package id="Microsoft.TeamFoundation.DistributedTask.Common.Contracts" version="15.122.1-preview" targetFramework="net452" />
|
||||
<package id="Microsoft.TeamFoundationServer.Client" version="15.122.1-preview" targetFramework="net452" />
|
||||
<package id="Microsoft.Tpl.Dataflow" version="4.5.24" targetFramework="net452" />
|
||||
|
|
Загрузка…
Ссылка в новой задаче