Retrieving chatinfo and attendees using MS Graph SDK - microsoft-graph-api

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"

Related

How can I retrieve all members when querying for appRoleAssignedTo in Microsoft Graph?

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.

Finding a message across multiple folder trees with one call with microsoft-graph java sdk

In converting from an older bit of code that uses the EWS (ews-java-api v 2.0) SDK/API/Scope to Graph (microsoft-graph v5.4.0), I found that I could search (say by InternetMessageId) across multiple folder hierarchies at once in EWS with (simplifying how to get FolderId values a bit):
SearchFilter.SearchFilterCollection filter =
new SearchFilter.SearchFilterCollection(LogicalOperator.And);
filter.add(new SearchFilter.IsEqualTo(EmailMessageSchema.InternetMessageId, msgId));
List<FolderId> folders = Arrays.asList(new FolderId("AllItems"), new FolderId("Deletions"));
ItemView view = new ItemView(10);
ServiceResponseCollection<FindItemResponse<Item>> findResultsCollection =
service.findItems(searchFolders, filter, null, view, null, ReturnErrors);
With that EWS search whether my message of interest is in the Inbox, some user-created sub-folder, JunkEmail, DeletedItems, RecoverableItemsDeletions I find it by InternetMessageId in one go.
With Graph I issue two calls to be able to ensure the message does not exist
UserRequestBuilder u = GraphServiceClient
.builder()
.authenticationProvider(authenticationProvider)
.buildClient()
.users(user);
for (String folderTree : Arrays.asList("AllItems", "RecoverableItemsDeletions")) {
MessageCollectionPage mcp = u.mailFolders(folderTree)
.messages()
.buildRequest()
.filter("internetMessageId eq '" + msgId + "'")
.get();
Is there a way to search multiple trees in one go with Graph to be more like the EWS path that took a List?
Use List Messages endpoint to get the messages in the signed-in user's mailbox (including the Deleted Items and Clutter folders).
Depending on the page size and mailbox data, getting messages from a mailbox can incur multiple requests. The default page size is 10 messages. Use $top to customize the page size, within the range of 1 and 1000.
To improve the operation response time, use $select to specify the exact properties you need. Refer documentation here.
Java Code Snippet -
GraphServiceClient graphClient = GraphServiceClient.builder().authenticationProvider( authProvider ).buildClient();
MessageCollectionPage messages = graphClient.me().messages()
.buildRequest()
.select("sender,subject")
.get();

Error when querying Microsoft Graph API Shifts: "MS-APP-ACTS-AS header needs to be set for application context requests"

We are trying to query shifts in the Microsoft Graph API using a C# app, now that StaffHub got deprecated , in the past we were getting an Unknown Error which looked like a permissions issue.
In the docs I noticed permissions for Schedule.ReadAll and Schedule.ReadWriteAll so I added them to the application permissions in our App Registration in Azure.
Now when we send the request to https://graph.microsoft.com/beta/teams/{teamid}/schedule we get this error:
Microsoft.Graph.ServiceException: 'Code: Forbidden Message: {"error":{"code":"Forbidden","message":"MS-APP-ACTS-AS header needs to be set for application context requests.","details":[],"innererror":{"code":"MissingUserIdHeaderInAppContext"}}}
The documentation says the Schedule permissions are in private preview, are these required for querying a schedule & shifts, and if so, is it possible to request access to the private preview?
I'm in the same situation. It's possible to request private preview access (we have), but I'm guessing that it's primarily granted to Microsoft partners or at least have a connection at Microsoft.
The workaround for me has been getting access on behalf of a user. It does however require the user to enter username and password in order to get an access token, so it might not be a perfect solution for you. But it works. You need to add (and, I believe, grant admin consent for) delegated permissions for this to work, either Group.Read.All or Group.ReadWrite.All.
Edit:
I've got it working now. We have private preview access, so I'm not sure this will help you unless you do too, but as I understand it will be available eventually. Given your question, I presume you already have an access token.
Add MS-APP-ACT-AS as a header with the user ID of the user you want the Graph client to act as.
If you're using the Graph SDK for .NET Core you can just add a header to the authentication provider:
public IAuthenticationProvider GetAuthenticationProviderForActingAsUser(string userId, string accessToken)
{
return new DelegateAuthenticationProvider(
requestMessage =>
{
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
requestMessage.Headers.Add("MS-APP-ACTS-AS", userId);
return Task.CompletedTask;
});
}
Then you call the graph service client:
var authenticationProvider = GetAuthenticationProviderForActingAsUser(userId, accessToken);
var graphClient = new GraphServiceClient(authenticationProvider);
You should then be able to fetch the shifts:
var shifts = await graphClient.Teams[teamId].Schedule.Shifts
.Request()
.AddAsync(shift);

How to search the group by the DisplayName using Microsoft Graph?

According to the document, I can list the Office 365 Groups by using the following Graph API:
GET https://graph.microsoft.com/v1.0/groups
I have a C# Web application, and there is a input for searching by the Group DisplayName. Any idea how to query groups based on the DisplayName?
I have tried the following URL: https://graph.microsoft.com/v1.0/groups?$search="displayName:Test" in the MS Graph Explorer which didn't work.
I get the following error.
{
"error": {
"code": "Request_UnsupportedQuery",
"message": "This query is not supported.",
"innerError": {
"request-id": "35d90412-03f3-44e7-a7a4-d33cee155101",
"date": "2018-10-25T05:32:53"
}
}
Any suggestion is welcomed.
Thanks in advance.
According to your description, I assume you want to search the Group by the DisplayName using the search parameters.
Based on this document, we can currently search only message and person collections. So we couldn't use the search parameter.
We can use the filter query parameter to search the Group by DisplayName. For example, we can search the groups whose displayName is start with 'Test',the request url like this:
https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'Test')
Here is C# code that I wrote to get a group using the DisplayName. This code requires a reference to the OfficeDevPnP.Core.
private static async Task<Group> GetGroupByName(string accessToken, string groupName)
{
var graphClient = GraphUtility.CreateGraphClient(accessToken);
var targetGroupCollection = await graphClient.Groups.Request()
.Filter($"startsWith(displayName,'{groupName}')")
.GetAsync();
var targetGroup = targetGroupCollection.ToList().Where(g => g.DisplayName == groupName).FirstOrDefault();
if (targetGroup != null)
return targetGroup;
return null;
}
UPDATE
I see that the answer has already been accepted, but I came across the same issue and found that this answer is out of date. For the next person, this is the update:
The 'search' functionality does work. Whether it was fixed along the way or always has, I am not sure.
'groups' support search,
both the v1 and beta api support search,
search only works on 'displayName' and 'description' fields,
searching on 'directory objects' require a special header: 'ConsistencyLevel: eventual'
Point number 4 is what tripped me up!
Your request would look like this:
https://graph.microsoft.com/v1.0/groups?$search="displayName:Test"
With the request header:
ConsistencyLevel: eventual
There is another catch: You can only specify the first 21 characters and the search always uses 'startsWith'. You're out of luck if you specify more than that: The search always fails.

Unable to save a query as a view table

I have a query that runs and can see the results. But while trying to save the query as a view table, I get error message saying
Failed to save view. No suitable credentials found to access Google
Drive. Contact the table owner for assistance.
I think the problem is caused by a table used in the query. The table is uploaded from a google sheet (with source URI), own by me. I have tried to enable Google Drive API from the project but no luck. Not sure how I can give BigQuery access to Google Drive.
I suspect the problem you are hitting is one of OAuth Scopes. In order to talk to the Google Drive API to read data, you need to use credentials that were granted access to that API.
If you are using the BigQuery web UI and have not explicitly granted access to Drive, it won't work. For example, the first time I tried to "Save to Google Sheets", the BigQuery UI popped up an OAuth prompt asking me to grant access to my Google Drive. After this it could save the results. Try doing this to make sure your credentials have the Drive scope and then "Save View" again.
If you are using your own code to do this, you should request scope 'https://www.googleapis.com/auth/drive' in addition to the 'https://www.googleapis.com/auth/bigquery' scope you are already using to talk to BigQuery.
If you are using the bq client, it has been updated to request this scope, but you may need to re-initialize your authentication credentials. You can do this with bq init --delete_credentials to remove the credentials, then your next action we re-request credentials.
Using Google App Script this worked for me:
function saveQueryToTable() {
var projectId = '...yourprojectid goes here...';
var datasetId = '...yourdatesetid goes here...';
var sourceTable = '...your table or view goes here...';
var destTable = '...destination table goes here...';
var myQuery;
//just a random call to activate the Drive API scope
var test = Drive.Properties.list('...drive file id goes here...')
//list all tables for the particular dataset
var tableList = BigQuery.Tables.list(projectId, datasetId).getTables();
//if the table exist, delete it
for (var i = 0; i < tableList.length; i++) {
if (tableList[i].tableReference.tableId == destTable) {
BigQuery.Tables.remove(projectId, datasetId, destTable);
Logger.log("DELETED: " + destTable);
}
};
myQuery = 'SELECT * FROM [PROJECTID:DATASETID.TABLEID];'
.replace('PROJECTID',projectId)
.replace('DATASETID',datasetId)
.replace('TABLEID',sourceTable)
var job = {
configuration: {
query: {
query: myQuery,
destinationTable: {
projectId: projectId,
datasetId: datasetId,
tableId: destTable
}
}
}
};
var queryResults = BigQuery.Jobs.insert(job, projectId);
Logger.log(queryResults.status);
}
The 'trick' was a random call to the Drive API to ensure both the BigQuery and Drive scopes are included.
Google Apps Script Project Properties

Resources