From e1e95f360aef626d16667053abe9157b68b18262 Mon Sep 17 00:00:00 2001 From: "Justin Marks (MSFT)" Date: Wed, 7 Feb 2018 11:34:22 -0800 Subject: [PATCH] Adding sample for fully enumerating users of a group --- .../dotnet/GraphQuickStarts/App.config | 4 +- .../GraphQuickStarts/GraphQuickStarts.csproj | 13 +- .../dotnet/GraphQuickStarts/Program.cs | 31 +- .../Samples/EnumerateMembersOfGroups.cs | 295 ++++++++++++++++++ .../Samples/EnumerateUsers.cs | 24 +- .../dotnet/GraphQuickStarts/packages.config | 2 +- 6 files changed, 345 insertions(+), 24 deletions(-) create mode 100644 ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateMembersOfGroups.cs diff --git a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/App.config b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/App.config index ebd22d2..6eed2f8 100644 --- a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/App.config +++ b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/App.config @@ -42,7 +42,7 @@ - + @@ -50,7 +50,7 @@ - + diff --git a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/GraphQuickStarts.csproj b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/GraphQuickStarts.csproj index 3a8887b..d0d8206 100644 --- a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/GraphQuickStarts.csproj +++ b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/GraphQuickStarts.csproj @@ -33,11 +33,11 @@ 4 - - packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.5\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll + + packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.dll - - packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.5\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll + + packages\Microsoft.IdentityModel.Clients.ActiveDirectory.3.13.8\lib\net45\Microsoft.IdentityModel.Clients.ActiveDirectory.Platform.dll packages\WindowsAzure.ServiceBus.3.3.2\lib\net45-full\Microsoft.ServiceBus.dll @@ -115,10 +115,13 @@ + - + + Designer + diff --git a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Program.cs b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Program.cs index cd405d7..8db95ab 100644 --- a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Program.cs +++ b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Program.cs @@ -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 argsMap = new Dictionary(); foreach (var arg in args) @@ -89,7 +96,19 @@ namespace GraphQuickStarts case "token": 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); } diff --git a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateMembersOfGroups.cs b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateMembersOfGroups.cs new file mode 100644 index 0000000..857ef4d --- /dev/null +++ b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateMembersOfGroups.cs @@ -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 + + /// + /// Constructor. Manaully set values to match your account. + /// + 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 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()) + { + // 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 expandedUsers = new List(); + 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 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 lookupKeys = new List(); + List memberships = graphClient.GetMembershipsAsync(group.Descriptor, Microsoft.VisualStudio.Services.Graph.GraphTraversalDirection.Down).Result; + foreach (var membership in memberships) + { + lookupKeys.Add(new GraphSubjectLookupKey(membership.MemberDescriptor)); + } + IReadOnlyDictionary 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 ExpandAadGroups(string accessToken, GraphGroup group) + { + //List of users in an AAD group + List aadUsers = new List(); + + //Getting all members in all groups and nesteed groups + List members = new List(); + members.AddRange(GetAADGroupMembers(accessToken, group.OriginId)); + while (members.Count != 0) + { + List nestedGroups = new List(); + 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 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(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 Users; + public List AADGroups; + + public GroupMemberships() + { + Users = new List(); + AADGroups = new List(); + } + + 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 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 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 +} diff --git a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateUsers.cs b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateUsers.cs index 86b16b4..97b6e95 100644 --- a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateUsers.cs +++ b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/Samples/EnumerateUsers.cs @@ -44,18 +44,22 @@ namespace GraphQuickStarts.Samples List graphUsers = new List(users.GraphUsers); // If there are more than a page's worth of users, continue retrieving users from the server a page at a time - string continuationToken = users.ContinuationToken.FirstOrDefault(); - while (continuationToken != null) + if (users.ContinuationToken != null) { - users = graphClient.GetUsersAsync(continuationToken: continuationToken).Result; - graphUsers.AddRange(users.GraphUsers); - - if (users.ContinuationToken != null) { - continuationToken = users.ContinuationToken.FirstOrDefault(); - } - else + string continuationToken = users.ContinuationToken.FirstOrDefault(); + while (continuationToken != null) { - break; + users = graphClient.GetUsersAsync(continuationToken: continuationToken).Result; + graphUsers.AddRange(users.GraphUsers); + + if (users.ContinuationToken != null) + { + continuationToken = users.ContinuationToken.FirstOrDefault(); + } + else + { + break; + } } } diff --git a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/packages.config b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/packages.config index dea872e..6a7d7eb 100644 --- a/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/packages.config +++ b/ClientLibrary/Quickstarts/dotnet/GraphQuickStarts/packages.config @@ -1,7 +1,7 @@  - +