536 lines
19 KiB
C#
536 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
using C4IT.Logging;
|
|
|
|
|
|
namespace C4IT.MsGraph
|
|
{
|
|
public class cMsGraphBase
|
|
{
|
|
public enum eHttpMethod { get, post, put, delete};
|
|
|
|
public const string constAzureLoginUrl = "https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/token";
|
|
public const string constAzureAuthorizeUrl = "https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/authorize";
|
|
public const string constMsGraphUrl = "https://graph.microsoft.com/{Version}/{Request}";
|
|
public const string constMsGraphApplicationGet = "applications?$filter=appId eq '{AppID}'";
|
|
public const string constMsGraphProfileGet = "me";
|
|
|
|
public bool IsOnline { get; private set; } = false;
|
|
|
|
public string AccessToken { get; private set; } = null;
|
|
public DateTime TokenExpiresIn { get; private set; } = DateTime.MinValue;
|
|
|
|
private cMsGraphLogonInfo privLogonInfo = null;
|
|
|
|
public Exception LastException { get; private set; } = null;
|
|
public string LastErrorMessage { get; private set; } = null;
|
|
|
|
public cMsGraphResultApplication Me { get; private set; } = null;
|
|
|
|
public HttpStatusCode? LastHttpErrorCode { get; private set; } = null;
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void ResetError()
|
|
{
|
|
LastException = null;
|
|
LastErrorMessage = null;
|
|
LastHttpErrorCode = null;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public void SetErrorException(string Action, Exception E, LogLevels lev = LogLevels.Error)
|
|
{
|
|
LastException = E;
|
|
LastErrorMessage = Action + ": " + E.Message;
|
|
cLogManager.LogEntry(Action, lev);
|
|
}
|
|
|
|
private async Task<bool> privLogonAsync(cMsGraphLogonInfo LogonInfo)
|
|
{
|
|
try
|
|
{
|
|
ResetError();
|
|
IsOnline = false;
|
|
|
|
// prepare the http login request
|
|
var postData = new List<KeyValuePair<string, string>>
|
|
{
|
|
new KeyValuePair<string, string>("client_id", LogonInfo.ClientID),
|
|
new KeyValuePair<string, string>("scope", LogonInfo.Scope),
|
|
new KeyValuePair<string, string>("redirect_uri", $"https://login.microsoftonline.com/{LogonInfo.Tenant}/oauth2/nativeclient"),
|
|
new KeyValuePair<string, string>("grant_type", "client_credentials"),
|
|
new KeyValuePair<string, string>("client_secret", LogonInfo.ClientSecret)
|
|
};
|
|
|
|
var formData = new FormUrlEncodedContent(postData);
|
|
var strUrl = constAzureLoginUrl.Replace("{Tenant}", LogonInfo.Tenant);
|
|
var request = new HttpRequestMessage(System.Net.Http.HttpMethod.Post, strUrl)
|
|
{
|
|
Content = formData
|
|
};
|
|
|
|
|
|
// do the http login request
|
|
using (var httpClient = new HttpClient())
|
|
{
|
|
var response = await httpClient.SendAsync(request);
|
|
LastHttpErrorCode = response.StatusCode;
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
LastErrorMessage = $"HTTP request error {response.StatusCode} while Azure tenant login: {response.ReasonPhrase}";
|
|
return false;
|
|
}
|
|
|
|
// get the response content & the access token
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
dynamic iToken = JsonConvert.DeserializeObject(content);
|
|
AccessToken = iToken.access_token;
|
|
int ExpiresIn = iToken.expires_in;
|
|
TokenExpiresIn = DateTime.UtcNow + TimeSpan.FromSeconds(ExpiresIn * 0.7);
|
|
var TokenType = iToken.token_type;
|
|
if (TokenType != "Bearer")
|
|
return false;
|
|
}
|
|
|
|
if (AccessToken != null)
|
|
{
|
|
IsOnline = true;
|
|
Me = await RequestApplicationAsync(LogonInfo.ClientID);
|
|
return true;
|
|
}
|
|
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
SetErrorException("HTTP exception error while Azure tenant login", E, LogLevels.Debug);
|
|
cLogManager.LogException(E, LogLevels.Debug);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private async Task<bool> privRelogon()
|
|
{
|
|
if (DateTime.UtcNow < TokenExpiresIn)
|
|
return true;
|
|
if (privLogonInfo == null)
|
|
return false;
|
|
var RetVal = await privLogonAsync(privLogonInfo);
|
|
return RetVal;
|
|
}
|
|
|
|
public async Task<bool> LogonAsync(cMsGraphLogonInfo LogonInfo)
|
|
{
|
|
var RetVal = await privLogonAsync(LogonInfo);
|
|
if (RetVal == true)
|
|
privLogonInfo = LogonInfo;
|
|
return RetVal;
|
|
}
|
|
|
|
private async Task<dynamic> privRequestAsync(string Url, eHttpMethod httpMethod = eHttpMethod.get, object JsonData = null, bool retryForbidden = false)
|
|
{
|
|
ResetError();
|
|
try
|
|
{
|
|
await RequestCriticalSection.Semaphore.WaitAsync();
|
|
|
|
if (!IsOnline)
|
|
return null;
|
|
|
|
if (!await privRelogon())
|
|
return null;
|
|
|
|
var retryCount = 0;
|
|
while (true)
|
|
{
|
|
// create the request
|
|
using (var handler = new HttpClientHandler())
|
|
{
|
|
using (var client = new HttpClient(handler))
|
|
{
|
|
client.DefaultRequestHeaders.Accept.Clear();
|
|
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
|
client.DefaultRequestHeaders.Authorization =
|
|
new AuthenticationHeaderValue("Bearer", AccessToken);
|
|
HttpResponseMessage response;
|
|
StringContent CO = null;
|
|
if (JsonData != null)
|
|
{
|
|
var strJson = JsonConvert.SerializeObject(JsonData);
|
|
CO = new StringContent(strJson, Encoding.UTF8, "application/json");
|
|
}
|
|
switch (httpMethod)
|
|
{
|
|
case eHttpMethod.get:
|
|
response = await client.GetAsync(Url);
|
|
break;
|
|
case eHttpMethod.post:
|
|
response = await client.PostAsync(Url, CO);
|
|
break;
|
|
case eHttpMethod.delete:
|
|
response = await client.DeleteAsync(Url);
|
|
break;
|
|
case eHttpMethod.put:
|
|
response = await client.PutAsync(Url, CO);
|
|
break;
|
|
default:
|
|
return null;
|
|
|
|
}
|
|
|
|
LastHttpErrorCode = response.StatusCode;
|
|
if (!response.IsSuccessStatusCode)
|
|
{
|
|
if (retryForbidden && (LastHttpErrorCode == HttpStatusCode.Forbidden) && (retryCount < 2))
|
|
{
|
|
retryCount++;
|
|
await (Task.Delay(100));
|
|
}
|
|
else
|
|
{
|
|
LastErrorMessage = $"HTTP request error {(int)response.StatusCode} while MS Graph request '{Url}': {response.ReasonPhrase}";
|
|
if (cLogManager.DefaultLogger.IsDebug)
|
|
cLogManager.DefaultLogger.LogEntry(LogLevels.Warning, LastErrorMessage);
|
|
return null;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (response.StatusCode == HttpStatusCode.NoContent)
|
|
return true;
|
|
|
|
var content = await response.Content.ReadAsStringAsync();
|
|
dynamic jsonData = JsonConvert.DeserializeObject(content); ;
|
|
|
|
return jsonData;
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
SetErrorException("HTTP exception error while while MS Graph request '{Request}'", E);
|
|
cLogManager.LogException(E);
|
|
}
|
|
finally
|
|
{
|
|
RequestCriticalSection.Semaphore.Release();
|
|
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async Task<cMsGraphResultBase> RequestAsync(string Request, bool UseBeta = false, eHttpMethod httpMethod = eHttpMethod.get, object JsonData = null, bool retryForbidden = false)
|
|
{
|
|
try
|
|
{
|
|
// get the request url
|
|
var strVersion = "v1.0";
|
|
if (UseBeta)
|
|
strVersion = "beta";
|
|
var strUrl = constMsGraphUrl.Replace("{Version}", strVersion);
|
|
strUrl = strUrl.Replace("{Request}", Request);
|
|
|
|
var data = await privRequestAsync(strUrl, httpMethod, JsonData, retryForbidden);
|
|
if (data == null)
|
|
return null;
|
|
|
|
if (data is bool isValid)
|
|
{
|
|
if (isValid)
|
|
return new cMsGraphResultBase(null);
|
|
else
|
|
return null;
|
|
}
|
|
|
|
var RetVal = new cMsGraphResultBase(data);
|
|
return RetVal;
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogException(E);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task<cMsGraphResultList> RequestListAsync(string Request, bool UseBeta = false, bool loadPaged = false, bool retryForbidden = false)
|
|
{
|
|
try
|
|
{
|
|
// get the request url
|
|
var strVersion = "v1.0";
|
|
if (UseBeta)
|
|
strVersion = "beta";
|
|
var strUrl = constMsGraphUrl.Replace("{Version}", strVersion);
|
|
strUrl = strUrl.Replace("{Request}", Request);
|
|
|
|
var res = await privRequestAsync(strUrl, retryForbidden: retryForbidden);
|
|
if (res != null)
|
|
{
|
|
var RetVal = new cMsGraphResultList(retryForbidden);
|
|
RetVal.AddResult(res);
|
|
if (!RetVal.hasRemaining || loadPaged)
|
|
return RetVal;
|
|
while (RetVal.hasRemaining)
|
|
{
|
|
if (!await RequestNextAsync(RetVal))
|
|
return RetVal;
|
|
}
|
|
return RetVal;
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.DefaultLogger.LogException(E);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public async Task<bool> RequestNextAsync(cMsGraphResultList List)
|
|
{
|
|
try
|
|
{
|
|
var res = await privRequestAsync(List.NextResultUrl, retryForbidden: List.retryForbidden);
|
|
if (res != null)
|
|
{
|
|
List.AddResult(res);
|
|
return true;
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.DefaultLogger.LogException(E);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public async Task<cMsGraphResultUser> RequestProfileAsync()
|
|
{
|
|
ResetError();
|
|
try
|
|
{
|
|
var Result = await RequestAsync(constMsGraphProfileGet);
|
|
if (Result != null)
|
|
return new cMsGraphResultUser(Result);
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogException(E);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public async Task<cMsGraphResultApplication> RequestApplicationAsync(string AppID)
|
|
{
|
|
ResetError();
|
|
try
|
|
{
|
|
var strRequest = constMsGraphApplicationGet.Replace("{AppID}", AppID);
|
|
var Result = await RequestAsync(strRequest, retryForbidden: true);
|
|
if (Result != null)
|
|
{
|
|
dynamic lst = Result.Result.value;
|
|
if (Result.Result.value.Count >= 1)
|
|
{
|
|
dynamic app = Result.Result.value[0];
|
|
var RetVal = new cMsGraphResultBase(app);
|
|
return new cMsGraphResultApplication(RetVal);
|
|
}
|
|
|
|
return new cMsGraphResultApplication(Result);
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.LogException(E);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
}
|
|
|
|
public class cMsGraphLogonInfo
|
|
{
|
|
public string Tenant;
|
|
public string ClientID;
|
|
public string ClientSecret;
|
|
public string Scope = "https://graph.microsoft.com/.default";
|
|
}
|
|
|
|
public static class RequestCriticalSection
|
|
{
|
|
public static SemaphoreSlim Semaphore = new SemaphoreSlim(1, 1);
|
|
}
|
|
|
|
public class cMsGraphResultBase
|
|
{
|
|
public string ID { get; private set; } = null;
|
|
public string ODataId { get; private set; } = null;
|
|
public string DisplayName { get; private set; } = null;
|
|
|
|
public string Context { get; private set; } = null;
|
|
|
|
public dynamic Result { get; private set; } = null;
|
|
|
|
public cMsGraphResultBase(dynamic Result)
|
|
{
|
|
this.Result = Result;
|
|
|
|
try { ID = Result.id; } catch { };
|
|
try {
|
|
if (Result.TryGetValue("displayName", out JToken JT1))
|
|
DisplayName = Result.displayName;
|
|
else if (Result.TryGetValue("name", out JToken JT2))
|
|
DisplayName = Result.name;
|
|
}
|
|
catch { }
|
|
try { Context = Result["@odata.context"]; } catch { }
|
|
ODataId = GetStringFromDynamic(Result, "@odata.id");
|
|
if (string.IsNullOrEmpty(ODataId))
|
|
ODataId = @"https://graph.microsoft.com/v1.0/directoryObjects/" + ID;
|
|
}
|
|
|
|
public cMsGraphResultBase(cMsGraphResultBase Result)
|
|
{
|
|
if (Result == null)
|
|
return;
|
|
|
|
this.Result = Result.Result;
|
|
ID = Result.ID;
|
|
ODataId = Result.ODataId;
|
|
DisplayName = Result.DisplayName;
|
|
Context = Result.Context;
|
|
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public static string GetStringFromDynamic(dynamic O, string ProperyName)
|
|
{
|
|
try
|
|
{
|
|
return (string)O[ProperyName];
|
|
}
|
|
catch { }
|
|
return null;
|
|
}
|
|
}
|
|
|
|
public class cMsGraphResultList : List<cMsGraphResultBase>
|
|
{
|
|
public string NextResultUrl { get; private set; } = null;
|
|
public bool retryForbidden { get; private set; } = false;
|
|
|
|
public cMsGraphResultList(bool retryForbidden)
|
|
{
|
|
this.retryForbidden = retryForbidden;
|
|
}
|
|
|
|
public bool hasRemaining
|
|
{
|
|
get
|
|
{
|
|
return !string.IsNullOrEmpty(NextResultUrl);
|
|
}
|
|
}
|
|
|
|
public void AddResult(dynamic Result)
|
|
{
|
|
try
|
|
{
|
|
NextResultUrl = null;
|
|
|
|
if (Result.TryGetValue("@odata.nextLink", out JToken JT))
|
|
{
|
|
var JO = (JValue)JT;
|
|
|
|
NextResultUrl = Result["@odata.nextLink"];
|
|
}
|
|
|
|
if (Result.TryGetValue("value", out JToken JT2))
|
|
{
|
|
foreach (dynamic Entry in Result.value)
|
|
try
|
|
{
|
|
var val = new cMsGraphResultBase(Entry);
|
|
this.Add(val);
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.DefaultLogger.LogException(E);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception E)
|
|
{
|
|
cLogManager.DefaultLogger.LogException(E);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public class cMsGraphResultUser : cMsGraphResultBase
|
|
{
|
|
public string UserPrincipalName { get; private set; } = null;
|
|
public string EMail { get; private set; } = null;
|
|
public string GivenName { get; private set; } = null;
|
|
public string SurName { get; private set; } = null;
|
|
public string JobTitle { get; private set; } = null;
|
|
public string OfficeLocation { get; private set; } = null;
|
|
public string PreferredLanguage { get; private set; } = null;
|
|
|
|
public cMsGraphResultUser(cMsGraphResultBase Result) : base(Result)
|
|
{
|
|
UserPrincipalName = GetStringFromDynamic(Result.Result, "userPrincipalName");
|
|
EMail = GetStringFromDynamic(Result.Result, "mail");
|
|
GivenName = GetStringFromDynamic(Result.Result, "givenName");
|
|
SurName = GetStringFromDynamic(Result.Result, "surname");
|
|
JobTitle = GetStringFromDynamic(Result.Result, "jobTitle");
|
|
OfficeLocation = GetStringFromDynamic(Result.Result, "officeLocation");
|
|
PreferredLanguage = GetStringFromDynamic(Result.Result, "preferredLanguage");
|
|
}
|
|
|
|
}
|
|
|
|
public class cMsGraphCollectionUsers : Dictionary<string, cMsGraphResultUser>
|
|
{
|
|
public cMsGraphCollectionUsers() { }
|
|
public cMsGraphCollectionUsers(int n) : base(n) { }
|
|
|
|
public void Add(cMsGraphResultUser User)
|
|
{
|
|
if (!this.ContainsKey(User.ID))
|
|
this.Add(User.ID, User);
|
|
}
|
|
}
|
|
|
|
public class cMsGraphResultApplication : cMsGraphResultBase
|
|
{
|
|
public string AppID { get; private set; } = null;
|
|
public string PublisherDomain { get; private set; } = null;
|
|
|
|
public cMsGraphResultApplication(cMsGraphResultBase Result) : base(Result)
|
|
{
|
|
try { AppID = Result.Result.appId; } catch { };
|
|
try { PublisherDomain = Result.Result.publisherDomain; } catch { };
|
|
}
|
|
|
|
}
|
|
}
|