Restore MsTeams legacy permission compatibility
This commit is contained in:
@@ -7,13 +7,13 @@ using System.Threading.Tasks;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
using C4IT.Logging;
|
||||
using C4IT.MsGraph;
|
||||
using static C4IT.Logging.cLogManager;
|
||||
using C4IT.Matrix42.ServerInfo;
|
||||
using static C4IT.MsGraph.cMsGraphSharepoint;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using C4IT.Logging;
|
||||
using C4IT.MsGraph;
|
||||
using static C4IT.Logging.cLogManager;
|
||||
using C4IT.Matrix42.ServerInfo;
|
||||
using static C4IT.MsGraph.cMsGraphSharepoint;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace C4IT.LIAM
|
||||
{
|
||||
@@ -28,37 +28,93 @@ namespace C4IT.LIAM
|
||||
|
||||
public class cLiamProviderMsTeams : cLiamProviderBase
|
||||
{
|
||||
public readonly cMsGraphSharepoint MsSharepoint = new cMsGraphSharepoint(new cMsGraphBase());
|
||||
public const string allowedMailNickNameCharacter = @"[^A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]";
|
||||
|
||||
public readonly bool WithoutPrivateChannels = true;
|
||||
|
||||
private string lastErrorMessage = null;
|
||||
|
||||
private static readonly string[] RequiredGraphRoles = new[]
|
||||
{
|
||||
"Application.Read.All",
|
||||
"Channel.ReadBasic.All",
|
||||
"Directory.Read.All",
|
||||
"Files.ReadWrite.All",
|
||||
"Group.ReadWrite.All",
|
||||
"GroupMember.Read.All",
|
||||
"GroupMember.ReadWrite.All",
|
||||
"Team.Create",
|
||||
"Team.ReadBasic.All",
|
||||
"TeamSettings.Read.All",
|
||||
"User.Read.All",
|
||||
};
|
||||
|
||||
private void SetLastError(string message)
|
||||
{
|
||||
lastErrorMessage = string.IsNullOrWhiteSpace(message) ? null : message;
|
||||
}
|
||||
|
||||
public cLiamProviderMsTeams(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData) :
|
||||
base(LiamConfiguration, ProviderData)
|
||||
{
|
||||
WithoutPrivateChannels = AdditionalConfiguration.ContainsKey("WithoutPrivateChannels") ? AdditionalConfiguration["WithoutPrivateChannels"].ToLower() == "true" || AdditionalConfiguration["WithoutPrivateChannels"] == "1" : false;
|
||||
public readonly cMsGraphSharepoint MsSharepoint = new cMsGraphSharepoint(new cMsGraphBase());
|
||||
public const string allowedMailNickNameCharacter = @"[^A-Za-z0-9!#$%&'*+\-/=?^_`{|}~]";
|
||||
|
||||
public readonly bool WithoutPrivateChannels = true;
|
||||
|
||||
private string lastErrorMessage = null;
|
||||
|
||||
private sealed class GraphPermissionRequirement
|
||||
{
|
||||
public string Description { get; private set; }
|
||||
|
||||
public string[] AcceptedPermissions { get; private set; }
|
||||
|
||||
public GraphPermissionRequirement(string description, params string[] acceptedPermissions)
|
||||
{
|
||||
Description = description;
|
||||
AcceptedPermissions = acceptedPermissions ?? new string[0];
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly GraphPermissionRequirement[] RequiredGraphPermissions = new[]
|
||||
{
|
||||
new GraphPermissionRequirement(
|
||||
"Team lesen",
|
||||
"Team.ReadBasic.All",
|
||||
"Group.Read.All",
|
||||
"Group.ReadWrite.All",
|
||||
"Directory.Read.All",
|
||||
"Directory.ReadWrite.All"),
|
||||
new GraphPermissionRequirement(
|
||||
"Channels lesen",
|
||||
"Channel.ReadBasic.All",
|
||||
"ChannelSettings.Read.All",
|
||||
"ChannelSettings.ReadWrite.All",
|
||||
"Group.Read.All",
|
||||
"Group.ReadWrite.All",
|
||||
"Directory.Read.All",
|
||||
"Directory.ReadWrite.All"),
|
||||
new GraphPermissionRequirement(
|
||||
"Dateien lesen und schreiben",
|
||||
"Files.ReadWrite.All",
|
||||
"Sites.ReadWrite.All",
|
||||
"Sites.FullControl.All"),
|
||||
new GraphPermissionRequirement(
|
||||
"Benutzer lesen",
|
||||
"User.Read.All",
|
||||
"User.ReadWrite.All",
|
||||
"Directory.Read.All",
|
||||
"Directory.ReadWrite.All"),
|
||||
};
|
||||
|
||||
private static readonly GraphPermissionRequirement[] CloneBaseGraphPermissions = new[]
|
||||
{
|
||||
new GraphPermissionRequirement("Teams klonen", "Team.Create"),
|
||||
};
|
||||
|
||||
private static readonly GraphPermissionRequirement[] CloneAppsGraphPermissions = new[]
|
||||
{
|
||||
new GraphPermissionRequirement("Apps mitklonen", "Application.Read.All", "Application.ReadWrite.All"),
|
||||
};
|
||||
|
||||
private static readonly GraphPermissionRequirement[] CloneSettingsGraphPermissions = new[]
|
||||
{
|
||||
new GraphPermissionRequirement("Team-Einstellungen mitklonen", "TeamSettings.Read.All", "TeamSettings.ReadWrite.All"),
|
||||
};
|
||||
|
||||
private static readonly GraphPermissionRequirement[] CloneMemberGraphPermissions = new[]
|
||||
{
|
||||
new GraphPermissionRequirement(
|
||||
"Mitglieder mitklonen",
|
||||
"GroupMember.Read.All",
|
||||
"GroupMember.ReadWrite.All",
|
||||
"Group.Read.All",
|
||||
"Group.ReadWrite.All",
|
||||
"Directory.Read.All",
|
||||
"Directory.ReadWrite.All"),
|
||||
};
|
||||
|
||||
private void SetLastError(string message)
|
||||
{
|
||||
lastErrorMessage = string.IsNullOrWhiteSpace(message) ? null : message;
|
||||
}
|
||||
|
||||
public cLiamProviderMsTeams(cLiamConfiguration LiamConfiguration, cLiamProviderData ProviderData) :
|
||||
base(LiamConfiguration, ProviderData)
|
||||
{
|
||||
WithoutPrivateChannels = AdditionalConfiguration.ContainsKey("WithoutPrivateChannels") ? AdditionalConfiguration["WithoutPrivateChannels"].ToLower() == "true" || AdditionalConfiguration["WithoutPrivateChannels"] == "1" : false;
|
||||
}
|
||||
|
||||
public override async Task<bool> LogonAsync()
|
||||
@@ -68,90 +124,90 @@ namespace C4IT.LIAM
|
||||
|
||||
public async Task<bool> LogonAsync(bool force = false)
|
||||
{
|
||||
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId))
|
||||
{
|
||||
LogEntry($"Error: License not valid", LogLevels.Error);
|
||||
SetLastError("License not valid");
|
||||
return false;
|
||||
}
|
||||
if (!force && this.MsSharepoint.Base.IsOnline)
|
||||
{
|
||||
if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken))
|
||||
return false;
|
||||
SetLastError(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
{
|
||||
var LI = new cMsGraphLogonInfo() {
|
||||
Tenant = this.Domain,
|
||||
ClientID = this.Credential?.Identification,
|
||||
ClientSecret = this.Credential?.Secret
|
||||
};
|
||||
var RetVal = await MsSharepoint.Base.LogonAsync(LI);
|
||||
if (!RetVal)
|
||||
{
|
||||
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "MsTeams Logon fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken))
|
||||
return false;
|
||||
SetLastError(null);
|
||||
return RetVal;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
LogException(E);
|
||||
SetLastError(E.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogMethodEnd(CM);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(lastErrorMessage))
|
||||
SetLastError("MsTeams Logon fehlgeschlagen");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async Task<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1)
|
||||
{
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
{
|
||||
if (!await LogonAsync())
|
||||
return null;
|
||||
|
||||
var DataAreas = new List<cLiamDataAreaBase>();
|
||||
|
||||
var DAL = await MsSharepoint.RequestTeamsListAsync();
|
||||
if (DAL == null)
|
||||
{
|
||||
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "Konnte Teams-Liste nicht abrufen");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var Entry in DAL)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.DataAreaRegEx) && !Regex.Match(Entry.Key, this.DataAreaRegEx).Success)
|
||||
continue;
|
||||
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId))
|
||||
{
|
||||
LogEntry($"Error: License not valid", LogLevels.Error);
|
||||
SetLastError("License not valid");
|
||||
return false;
|
||||
}
|
||||
if (!force && this.MsSharepoint.Base.IsOnline)
|
||||
{
|
||||
if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken))
|
||||
return false;
|
||||
SetLastError(null);
|
||||
return true;
|
||||
}
|
||||
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
{
|
||||
var LI = new cMsGraphLogonInfo() {
|
||||
Tenant = this.Domain,
|
||||
ClientID = this.Credential?.Identification,
|
||||
ClientSecret = this.Credential?.Secret
|
||||
};
|
||||
var RetVal = await MsSharepoint.Base.LogonAsync(LI);
|
||||
if (!RetVal)
|
||||
{
|
||||
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "MsTeams Logon fehlgeschlagen");
|
||||
return false;
|
||||
}
|
||||
if (!EnsureGraphPermissions(MsSharepoint.Base?.AccessToken))
|
||||
return false;
|
||||
SetLastError(null);
|
||||
return RetVal;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
LogException(E);
|
||||
SetLastError(E.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogMethodEnd(CM);
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(lastErrorMessage))
|
||||
SetLastError("MsTeams Logon fehlgeschlagen");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override async Task<List<cLiamDataAreaBase>> getDataAreasAsync(int Depth = -1)
|
||||
{
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
{
|
||||
if (!await LogonAsync())
|
||||
return null;
|
||||
|
||||
var DataAreas = new List<cLiamDataAreaBase>();
|
||||
|
||||
var DAL = await MsSharepoint.RequestTeamsListAsync();
|
||||
if (DAL == null)
|
||||
{
|
||||
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? "Konnte Teams-Liste nicht abrufen");
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var Entry in DAL)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(this.DataAreaRegEx) && !Regex.Match(Entry.Key, this.DataAreaRegEx).Success)
|
||||
continue;
|
||||
|
||||
var MsTeam = await MsSharepoint.RequestGroupInfoAsync(Entry.Value);
|
||||
if (MsTeam == null)
|
||||
{
|
||||
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? $"Konnte Team-Informationen für '{Entry.Key}' nicht abrufen");
|
||||
continue;
|
||||
}
|
||||
|
||||
var Team = new cLiamMsTeamsTeam(this, MsTeam);
|
||||
DataAreas.Add(Team);
|
||||
}
|
||||
|
||||
var MsTeam = await MsSharepoint.RequestGroupInfoAsync(Entry.Value);
|
||||
if (MsTeam == null)
|
||||
{
|
||||
SetLastError(MsSharepoint.Base?.LastErrorMessage ?? $"Konnte Team-Informationen für '{Entry.Key}' nicht abrufen");
|
||||
continue;
|
||||
}
|
||||
|
||||
var Team = new cLiamMsTeamsTeam(this, MsTeam);
|
||||
DataAreas.Add(Team);
|
||||
}
|
||||
|
||||
if (Depth > 0)
|
||||
{
|
||||
var allChilds = new List<cLiamDataAreaBase>();
|
||||
@@ -161,97 +217,142 @@ namespace C4IT.LIAM
|
||||
if (entryChilds != null && entryChilds.Count > 0)
|
||||
allChilds.AddRange(entryChilds);
|
||||
}
|
||||
DataAreas.AddRange(allChilds);
|
||||
}
|
||||
|
||||
SetLastError(null);
|
||||
return DataAreas;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
LogException(E);
|
||||
SetLastError(E.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogMethodEnd(CM);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool EnsureGraphPermissions(string accessToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
SetLastError("Kein Access Token für Berechtigungsprüfung verfügbar");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parts = accessToken.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
SetLastError("Ungültiges Access Token");
|
||||
return false;
|
||||
}
|
||||
|
||||
var payload = parts[1].Replace('-', '+').Replace('_', '/');
|
||||
switch (payload.Length % 4)
|
||||
{
|
||||
case 2: payload += "=="; break;
|
||||
case 3: payload += "="; break;
|
||||
}
|
||||
|
||||
var payloadBytes = Convert.FromBase64String(payload);
|
||||
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
var payloadObj = JsonConvert.DeserializeObject<JObject>(payloadJson);
|
||||
if (payloadObj == null)
|
||||
{
|
||||
SetLastError("Token-Payload konnte nicht gelesen werden");
|
||||
return false;
|
||||
}
|
||||
|
||||
var granted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (payloadObj.TryGetValue("roles", out var rolesToken) && rolesToken is JArray roleArray)
|
||||
{
|
||||
foreach (var role in roleArray.Values<string>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(role))
|
||||
granted.Add(role);
|
||||
}
|
||||
}
|
||||
|
||||
if (!granted.Any() && payloadObj.TryGetValue("scp", out var scopeToken))
|
||||
{
|
||||
var scopes = scopeToken.Value<string>() ?? string.Empty;
|
||||
foreach (var scope in scopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
granted.Add(scope);
|
||||
}
|
||||
|
||||
var missing = RequiredGraphRoles.Where(required => !granted.Contains(required)).ToList();
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
SetLastError("Fehlende Graph-Berechtigungen: " + string.Join(", ", missing));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
SetLastError("Berechtigungsprüfung fehlgeschlagen: " + ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID)
|
||||
{
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
DataAreas.AddRange(allChilds);
|
||||
}
|
||||
|
||||
SetLastError(null);
|
||||
return DataAreas;
|
||||
}
|
||||
catch (Exception E)
|
||||
{
|
||||
LogException(E);
|
||||
SetLastError(E.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogMethodEnd(CM);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool EnsureGraphPermissions(string accessToken)
|
||||
{
|
||||
return EnsureGraphPermissions(accessToken, RequiredGraphPermissions, null);
|
||||
}
|
||||
|
||||
private bool EnsureClonePermissions(string accessToken, int partsToClone)
|
||||
{
|
||||
var requirements = new List<GraphPermissionRequirement>(CloneBaseGraphPermissions);
|
||||
var cloneParts = (CloneTeamRequest.ClonableTeamParts)partsToClone;
|
||||
|
||||
if (cloneParts.HasFlag(CloneTeamRequest.ClonableTeamParts.Apps))
|
||||
requirements.AddRange(CloneAppsGraphPermissions);
|
||||
if (cloneParts.HasFlag(CloneTeamRequest.ClonableTeamParts.Settings))
|
||||
requirements.AddRange(CloneSettingsGraphPermissions);
|
||||
if (cloneParts.HasFlag(CloneTeamRequest.ClonableTeamParts.Members))
|
||||
requirements.AddRange(CloneMemberGraphPermissions);
|
||||
|
||||
return EnsureGraphPermissions(accessToken, requirements, "Team-Klonen");
|
||||
}
|
||||
|
||||
private bool EnsureGraphPermissions(string accessToken, IEnumerable<GraphPermissionRequirement> requirements, string operationName)
|
||||
{
|
||||
if (!TryGetGrantedGraphPermissions(accessToken, out var granted, out var errorMessage))
|
||||
{
|
||||
SetLastError(errorMessage);
|
||||
return false;
|
||||
}
|
||||
|
||||
var missing = requirements
|
||||
.Where(requirement => requirement.AcceptedPermissions == null || !requirement.AcceptedPermissions.Any(granted.Contains))
|
||||
.Select(requirement => $"{requirement.Description} ({string.Join(" / ", requirement.AcceptedPermissions)})")
|
||||
.ToList();
|
||||
|
||||
if (missing.Count > 0)
|
||||
{
|
||||
var prefix = string.IsNullOrWhiteSpace(operationName)
|
||||
? "Fehlende Graph-Berechtigungen: "
|
||||
: $"Fehlende Graph-Berechtigungen für {operationName}: ";
|
||||
SetLastError(prefix + string.Join(", ", missing));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetGrantedGraphPermissions(string accessToken, out HashSet<string> granted, out string errorMessage)
|
||||
{
|
||||
granted = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
errorMessage = null;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(accessToken))
|
||||
{
|
||||
errorMessage = "Kein Access Token für Berechtigungsprüfung verfügbar";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var parts = accessToken.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
errorMessage = "Ungültiges Access Token";
|
||||
return false;
|
||||
}
|
||||
|
||||
var payload = parts[1].Replace('-', '+').Replace('_', '/');
|
||||
switch (payload.Length % 4)
|
||||
{
|
||||
case 2: payload += "=="; break;
|
||||
case 3: payload += "="; break;
|
||||
}
|
||||
|
||||
var payloadBytes = Convert.FromBase64String(payload);
|
||||
var payloadJson = Encoding.UTF8.GetString(payloadBytes);
|
||||
var payloadObj = JsonConvert.DeserializeObject<JObject>(payloadJson);
|
||||
if (payloadObj == null)
|
||||
{
|
||||
errorMessage = "Token-Payload konnte nicht gelesen werden";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payloadObj.TryGetValue("roles", out var rolesToken) && rolesToken is JArray roleArray)
|
||||
{
|
||||
foreach (var role in roleArray.Values<string>())
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(role))
|
||||
granted.Add(role);
|
||||
}
|
||||
}
|
||||
|
||||
if (!granted.Any() && payloadObj.TryGetValue("scp", out var scopeToken))
|
||||
{
|
||||
var scopes = scopeToken.Value<string>() ?? string.Empty;
|
||||
foreach (var scope in scopes.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries))
|
||||
granted.Add(scope);
|
||||
}
|
||||
|
||||
if (!granted.Any())
|
||||
{
|
||||
errorMessage = "Keine Graph-Berechtigungen im Access Token gefunden";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
errorMessage = "Berechtigungsprüfung fehlgeschlagen: " + ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task<cLiamDataAreaBase> LoadDataArea(string UID)
|
||||
{
|
||||
var CM = MethodBase.GetCurrentMethod();
|
||||
LogMethodBegin(CM);
|
||||
try
|
||||
{
|
||||
if (!cC4ITLicenseM42ESM.Instance.IsValid || !cC4ITLicenseM42ESM.Instance.Modules.ContainsKey(LiamInitializer.msTeamsModuleId))
|
||||
{
|
||||
@@ -340,15 +441,20 @@ namespace C4IT.LIAM
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public override string GetLastErrorMessage()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(lastErrorMessage))
|
||||
return lastErrorMessage;
|
||||
return MsSharepoint?.Base?.LastErrorMessage ?? string.Empty;
|
||||
}
|
||||
public override string GetLastErrorMessage()
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(lastErrorMessage))
|
||||
return lastErrorMessage;
|
||||
return MsSharepoint?.Base?.LastErrorMessage ?? string.Empty;
|
||||
}
|
||||
|
||||
public async Task<cMsGraphResultBase> cloneTeam(string teamId, string name, string description, int visibility, int partsToClone, string additionalMembers, string additionalOwners)
|
||||
{
|
||||
if (!await LogonAsync())
|
||||
return null;
|
||||
if (!EnsureClonePermissions(MsSharepoint.Base?.AccessToken, partsToClone))
|
||||
return null;
|
||||
|
||||
var request = new CloneTeamRequest()
|
||||
{
|
||||
DisplayName = name,
|
||||
|
||||
Reference in New Issue
Block a user