How can I retrieve all members when querying for appRoleAssignedTo in Microsoft Graph? - microsoft-graph-api

I am attempting to programmatically retrieve a list of users (principalType = "User") and their associated appRoleId values for an enterprise app using itsresourceId value from Azure AD. There is a total of ten Users with a combined total of twenty appRoleId values associated with the app. However, when I run my query I receive data for just two users and a combined total of four appRoleId values.
Here's my C# code:
GraphServiceClient myGraphClient = GetGraphServiceClient([scopes]);
// Retrieve the [Id] value for the app. Note [Id] is a pseudonym for the [resourceId] required to retrieve users and app roles assigned.
var servPrinPage = await myGraphClient.ServicePrincipals.Request()
.Select("id,appRoles")
.Filter($"startswith(displayName, 'Display Name')")
.GetAsync()
.ConfigureAwait(false);
// Using the first [Id] value from the [ServicePrincipals] page, retrieve the list of users and their assigned roles for the app.
var appRoleAssignedTo = await myGraphClient.ServicePrincipals[servPrinPage[0].Id].AppRoleAssignedTo.Request().GetAsync().ConfigureAwait(false);
The query returns a ServicePrincipalAppRoleAssignedToCollectionPage (as expected) but the collection only contains four pages (one per User/appRoleId combination).
As an aside, the following query in Microsoft Graph Explorer produces an equivalent result:
https://graph.microsoft.com/v1.0/servicePrincipals/[resourceId]/appRoleAssignedTo
What am I missing here? I need to be able to retrieve the complete list of users and assigned app roles. Any assistance is greatly appreciated.

The issue I was confronting has to do with the pagination feature employed by Azure AD and MS Graph. In a nutshell, I was forced to submit two queries in order to retrieve all twenty records I was expecting.
If you have a larger set of records to be retrieved you may be faced with submitting a much larger number of successive queries. The successive queries are managed using a "skiptoken" passed as a request header each time your query is resubmitted.
Here is my revised code with notation....
// Step #1: Create a class in order to strongly type the <List> which will hold your results.
// Not absolutely necessary but always a good idea when working with <Lists> in C#.
private class AppRoleByUser
{
public string AzureDisplayName;
public string PrincipalDisplayName;
}
// Step #2: Submit a query to acquire the [id] for the Service Principal (i.e. your app).
// Note the [ServicePrincipals].[id] property is synonymous with the [resourceId] needed to
// retrieve [AppRoleAssignedTo] values from Microsoft Graph in the next step.
// Initialize the Microsoft Graph Client.
GraphServiceClient myGraphClient = GetGraphServiceClient("Directory.Read.All");
// Retrieve the Service Principals page containing the app [Id].
var servPrinPage = await myGraphClient.ServicePrincipals.Request().Select("id,appRoles").Filter($"startswith(displayName, 'Your App Name')").GetAsync().ConfigureAwait(false);
// Store the app [Id] in a local variable (for readability).
string resourceId = servPrinPage[0].Id;
// Step #3: Using the [Id]/[ResourceId] value from the previous step, retrieve a list of AppRoleId/DisplayName pairs for your app.
// Results of the successive queries are typed against the class created earlier and are appended to the <List>.
List<AppRoleByUser> appRoleByUser = new List<AppRoleByUser>();
// Note, unlike "Filter" or "Search" parameters, it is not possible to
// add a "Skiptoken" parameter directly to your query in C#.
// Instead, it is necessary to insert the "skiptoken" as request header using the Graph QueryOption class.
// Note the QueryOption List is passed as an empty object on the first pass of the while loop.
var queryOptions = new List<QueryOption>();
// Initialize the variable to hold the anticipated query result.
ServicePrincipalAppRoleAssignedToCollectionPage appRoleAssignedTo = new ServicePrincipalAppRoleAssignedToCollectionPage();
// Note the number of user/role combinations associated with an app is not always known.
// Consequently, you may be faced with the need to acquire multiple pages
// (and submit multiple consecutive queries) in order to obtain a complete
// listing of user/role combinations.
// The "while" loop construct will be utilized to manage query iteration.
// Execution of the "while" loop will be stopped when the "bRepeat" variable is set to false.
bool bRepeat = true;
while (bRepeat == true)
{
appRoleAssignedTo = (ServicePrincipalAppRoleAssignedToCollectionPage) await myGraphClient.ServicePrincipals[resourceId].AppRoleAssignedTo.Request(queryOptions).GetAsync().ConfigureAwait(false);
foreach (AppRoleAssignment myPage in appRoleAssignedTo)
{
// I was not able to find a definitive answer in any of the documents I
// found but it appears the final record in the recordset carries a
// [PrincipalType] = "Group" (all others carry a [PrincipalType] = "User").
if (myPage.PrincipalType != "Group")
{
// Insert "User" data into the List<AppRoleByUser> collection.
appRoleByUser.Add(new AppRoleByUser{ AzureDisplayName = myPage.PrincipalDisplayName, AzureUserRole = myPage.AppRoleId.ToString() });
}
else
{
// The "bRepeat" variable is initially set to true and is set to
// false when the "Group" record is detected thus signaling
// task completion and closing execution of the "while" loop.
bRepeat = false;
}
}
// Acquire the "nextLink" string from the response header.
// The "nextLink" string contains the "skiptoken" string required for the next
// iteration of the query.
string nextLinkValue = appRoleAssignedTo.AdditionalData["#odata.nextLink"].ToString();
// Parse the "skiptoken" value from the response header.
string skipToken = nextLinkValue.Substring(nextLinkValue.IndexOf("=") + 1);
// Include the "skiptoken" as a request header in the next iteration of the query.
queryOptions = new List<QueryOption>()
{
new QueryOption("$skiptoken", skipToken)
};
}
That's a long answer to what should have been a simple question. I am relatively new to Microsoft Graph but it appears to me Microsoft has a long way to go in making this API developer-friendly. All I needed was to know the combination of AppRoles and Users associated with a single, given Azure AD app. One query and one response should have been more than sufficient.
At any rate, I hope my toil might help save time for someone else going forward.

Could you please remove "Filter" from the code and retry the operation. Let us know if that worked.

Related

Allowing only one answer in Google forms

Hi I want to create a Google form which i can collect student data,
And i want to make a way to allow my students to only enter one answer one time
in example I want to use this method in the question which asks for the admission number, admission number is a uniqe number which will not be repeated. So if a student enter an admission number that is already entered , I want them to see an error message with "The admission number has been already used"
Issue:
Avoid multiple form responses to be submitted with the same value for a specific form item.
Workflow:
In order to avoid certain responses to be submitted, you have to use some kind of data validation for the Admission number item, as shown in Set rules for your form.
This data validation should be updated every time a form response is submitted, since the newly submitted Admission number should be forbidden in future responses.
In order to update this validation every time the form is submitted, you could use an Apps Script onFormSubmit() trigger, which would then fire a function on every form submission. Let's call this function updateDataValidation.
The onFormSubmit trigger can be installed either manually, following these steps, or programmatically, via executing the function createTrigger from the code sample below once.
Every time updateDataValidation fires, it should do the following:
Find all the previous responses for your Admission number item. You could first retrieve all form responses via Form.getResponses() and then find the responses related to your item by checking its title (see Item.getTitle()).
Transform the array of admission numbers retrieved in previous step into a regex pattern that can be used for the data validation, which could be like this: ^{number1}$|^{number2}$|^{number3}$....
Use TextValidation in order to update the validation rules for your item.
Code sample:
function updateDataValidation(e) {
const itemTitle = "Admission number"; // Name of the form item. Change accordingly
const errorMessage = "The admission number has been already used.";
const form = e ? e.source : FormApp.getActiveForm();
const responses = form.getResponses();
const admissionNumbers = responses.map(response => {
return response.getItemResponses().find(itemResponse => {
return itemTitle === itemResponse.getItem().getTitle();
});
}).map(itemResponse => itemResponse.getResponse());
const numbersPattern = admissionNumbers.map(number => `^${number}\$`).join("|");
const item = form.getItems().find(item => item.getTitle() === itemTitle);
let validation = FormApp.createTextValidation()
.setHelpText(errorMessage)
.requireTextDoesNotMatchPattern(numbersPattern)
.build();
item.asTextItem().setValidation(validation);
}
function createTrigger() {
const form = FormApp.getActiveForm();
ScriptApp.newTrigger('updateDataValidation')
.forForm(form)
.onFormSubmit()
.create();
}
Notes:
In the code sample above, the item Admission number is assumed to be named Admission number. Change that from the code if that's not the case.
In the code sample above, the item Admission number is assumed to be a text item, like Short answer.

Retrieving chatinfo and attendees using MS Graph SDK

Starting with a sample oauth app I am trying to retrieve info about an online meeting that occurred.
Create the client:
var graphClient = _graphServiceClientFactory.GetAuthenticatedGraphClient((ClaimsIdentity)User.Identity);
Make request:
var returnObj = await graphClient.Me.OnlineMeetings.CreateOrGet(meetingID).Request().PostAsync();
The problem is the lack of information returned. I am trying to retrive the chat from the meeting, which seems like it should be in returnObj.ChatInfo but this is all I get back:
{
"threadId":"19:meeting_SOMELONGUNIQUESTRINGHERE#thread.v2",
"messageId":"0",
"#odata.type":"microsoft.graph.chatInfo"
}
Also missing are the attendees in Participants (count=0). I know there are non zero attendees and that a chat log exists.
Trying Select or Expand does not help. Select returns nothing new,and expand gives an error along the lines of Message: Parsing OData Select and Expand failed: Property 'participants' on type 'microsoft.graph.onlineMeeting' is not a navigation property or complex property. Only navigation properties can be expanded., and similarly for chatinfo.
Also, using the threadId I thought maybe I could do this:
var groups = await graphClient.Groups.Request().GetAsync();
Group group = groups[0];
ConversationThread chat;
chat = await graphClient.Groups[group.Id].Threads[chatId].Request().GetAsync();
where for chatId I used the threadId from chatinfo, wholey and parsed out in different ways but I get Not Found.
No idea if what I'm trying to do is even possible as the documentation is rather lacking in terms of tying different pieces together (Like what is the threadId for? where is it used?).
Also, here are the various scopes I am requesting
"GraphScopes": "User.Read User.ReadBasic.All Mail.Send OnlineMeetings.ReadWrite Group.Read.All Team.ReadBasic.All"

Best way to continue to query Quickbooks data

I have a problem with the qbxml.
I'm trying to migrate the qb customers, items etc to zohobooks.
I want to grab 50 customers first from quickbooks and calling zohobooks apis to create contacts on there. and again another 50 customers from quickbooks and to zohobooks.
The problem is I'm sure how can I continue to query after calling the zohobooks apis?
When I tried to use the same iteratorID from the first query response I got nothing from QB.
I'm building desktop app using .net, please advise me the best option to track the migration and where I'm.
Assume that I have 150 customers and for some reason stopped migrating after 100customers, in this case how can I get the last 50 customers next time?
public string customerQueryXml()
{
XmlDocument inputXMLDoc = new XmlDocument();
inputXMLDoc.AppendChild(inputXMLDoc.CreateXmlDeclaration("1.0", null, null));
inputXMLDoc.AppendChild(inputXMLDoc.CreateProcessingInstruction("qbposxml", "version=\"1.0\""));
XmlElement qbXML = inputXMLDoc.CreateElement("QBPOSXML");
inputXMLDoc.AppendChild(qbXML);
XmlElement qbXMLMsgsRq = inputXMLDoc.CreateElement("QBPOSXMLMsgsRq");
qbXML.AppendChild(qbXMLMsgsRq);
qbXMLMsgsRq.SetAttribute("onError", "stopOnError");
XmlElement customerQueryRq = inputXMLDoc.CreateElement("CustomerQueryRq");
qbXMLMsgsRq.AppendChild(customerQueryRq);
//customerQueryRq.SetAttribute("requestID", "1");
//customerQueryRq.SetAttribute("iterator", "Start");
customerQueryRq.SetAttribute("requestID", "2");
customerQueryRq.SetAttribute("iterator", "Continue");
customerQueryRq.SetAttribute("iteratorID", "{A1601C19-C6DC-43C0-AE43-6F45088C39F2}");
// for test only, read 10 customers
XmlElement MaxReturned = inputXMLDoc.CreateElement("MaxReturned");
customerQueryRq.AppendChild(MaxReturned).InnerText = "50";
XmlElement ownerID = inputXMLDoc.CreateElement("OwnerID");
customerQueryRq.AppendChild(ownerID).InnerText = "0";
XmlElement timeModifiedRangeFilter = inputXMLDoc.CreateElement("TimeModifiedRangeFilter");
customerQueryRq.AppendChild(timeModifiedRangeFilter);
XmlElement fromTimeModified = inputXMLDoc.CreateElement("FromTimeModified");
timeModifiedRangeFilter.AppendChild(fromTimeModified).InnerText = "1980-01-01T00:00:00";
XmlElement toTimeModified = inputXMLDoc.CreateElement("ToTimeModified");
timeModifiedRangeFilter.AppendChild(toTimeModified).InnerText = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss");
return inputXMLDoc.OuterXml;
}
EDIT:
I noticed that I have to use the iteratorID in the same request. By the way I have no problem with the qbxml itself.
My question is how can I continue to query the customers, items or whatever on another request?
ProcessRequest (first time)
migrated xml data to another system
and after that for whatever reason I stopped the request
here, can I continue to query on another ProcessRequest?
Iterators have to be used within a single Session. e.g. this will work:
Connect to QuickBooks (establish a session)
Do a request to create an iterator and get the first page of records
Do another request to continue the iterator
Do another request to continue the iterator
While this will not work, and is not something supported by QuickBooks:
Connect to QuickBooks (establish a session)
Do a request to create an iterator and get the first page of records
Disconnect
Do a request to create an iterator and get the first page of records

Get meetings by organizer or attendee email ID using GoToMeeting SDK

I am using .Net sdk of GoToMeeting.
I want to get meetings organized by particular organizer.
I have tried using
MeetingsApi.getHistoryMeetings but it does not return me OrganizerKey so I can not filter on particular Organizer.
Is there any way to get meeting(s) based on organizer or even by Attendee email ID by using .Net SDK?
What is the problem you are facing with MeetingsApi.getHistoryMeetings();?
why you need to filter the method, the MeetingsApi.getHistoryMeetings(accessToken,true,date1,date2); itself filtered for a particular user right?
Look on the arguments we are passing in the method?
accessToken - This token is generated as a result of successful authentication of a gotoproduct account. (In API call it can be generated using directlogin orOauth method.
true - this represents whether the meetings returned are past or not.
date1 - Start date range for the meetings.
date2 - End date range for the meetings.
below code is the sample for getting history meetings.
DateTime sdt=DateTime.Parse("07/01/2015");
DateTime edt=DateTime.Parse("07/30/2015");
List<MeetingHistory> historymeets = new System.Collections.Generic.List<MeetingHistory>();
historymeets=meeting.getHistoryMeetings(accesToken, true, sdt, edt);
foreach (var item in historymeets)
{
Console.WriteLine(item.subject);
}
try it out... The above code will store the meetings in historymeets collection object.
You can do the filter function in that collection object.
UPDATE :
List<MeetingHistory> historymeets = new System.Collections.Generic.List<MeetingHistory>();
historymeets=meeting.getHistoryMeetings(accesToken, true, sdt, edt);
List<AttendeeByMeeting> lstAttendee = new System.Collections.Generic.List<AttendeeByMeeting>();
foreach (var item in historymeets)
{
Console.WriteLine(item.meetingId);
lstAttendee=meeting.getAttendeesByMeetings(accesToken, item.meetingId);
foreach (var itemattendee in lstAttendee)
{
Console.WriteLine(itemattendee.attendeeEmail);
}
}
for comment - It is possible, but not directly because there is no api calls, which supports the meeting by attendee . the above code which i have written is for meeting by organizer . Now you have two options,
get the getHistoryMeetings, now you got the meeting details right? , then get the attendees by meeting id using getAttendeesByMeetings(), filter the two different collection objects with join using LINQ. OR
get the meetingdetails and attendees by executing two different fuinction calls, and store it in database or somewhere else, so that you can access it for doing the filter

Facebook connect graph status objects have comments capped at 25

Does anyone know why no matter how many comments a given graph status update object has, it will cap the comments at 25? I have a feeling it only returns a 'sample' of the actual comments on the object. How do I force it to get them all without using the FQL APIs?
This is just the way the Graph API works. Take a look at the API docs. You get 25 at a time and have to loop through them. You can use the timestamp (created_time) of the last comment in the batch as a parameter in the next Graph API call or you can use the offset parameter. Which is what I've been doing. I was running into some screwiness using created_time. This is an example from my C# test app. Ignore the references to the PostComment object that's just a data structure I created to hold the data I'm pulling. The magic (and the process i'm referencing) is in the parameters being passed to the graph API call:
parameters.Add("offset", numPostComments);
parameters.Add("limit", 25);
I'm fairly certain you can set the "limit" to anything 25 or below.
do
{
foreach (var comment in comments.data)
{
numPostComments++;
PostComment pc = new PostComment();
pc.Post_ID = p.Id;
pc.Facebook_ID = comment.id;
pc.From = comment.from.name;
if (comment.likes != null)
pc.Likes = (int)comment.likes;
pc.CommentDate = DateTime.Parse(comment.created_time);
pc.CommentText = comment.message;
p.Comments.Add(pc);
}
// Create new Parameters object for call to API
Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("offset", numPostComments);
parameters.Add("limit", 25);
// Call the API to get the next block of 25
comments = client.Get(string.Format("{0}/comments", p.Facebook_ID), parameters);
} while (comments.data.Count > 0);

Resources