Restore MsTeams legacy permission compatibility

This commit is contained in:
Meik
2026-03-29 22:58:29 +02:00
parent d95a9c0ea9
commit 54be771569

View File

@@ -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,