From 54be771569d8eb5cc6cc7a0a62193e7ef5521722 Mon Sep 17 00:00:00 2001 From: Meik Date: Sun, 29 Mar 2026 22:58:29 +0200 Subject: [PATCH] Restore MsTeams legacy permission compatibility --- LiamMsTeams/C4IT.LIAM.MsTeams.cs | 542 ++++++++++++++++++------------- 1 file changed, 324 insertions(+), 218 deletions(-) diff --git a/LiamMsTeams/C4IT.LIAM.MsTeams.cs b/LiamMsTeams/C4IT.LIAM.MsTeams.cs index 80332f1..3596bb1 100644 --- a/LiamMsTeams/C4IT.LIAM.MsTeams.cs +++ b/LiamMsTeams/C4IT.LIAM.MsTeams.cs @@ -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 LogonAsync() @@ -68,90 +124,90 @@ namespace C4IT.LIAM public async Task 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> getDataAreasAsync(int Depth = -1) - { - var CM = MethodBase.GetCurrentMethod(); - LogMethodBegin(CM); - try - { - if (!await LogonAsync()) - return null; - - var DataAreas = new List(); - - 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> getDataAreasAsync(int Depth = -1) + { + var CM = MethodBase.GetCurrentMethod(); + LogMethodBegin(CM); + try + { + if (!await LogonAsync()) + return null; + + var DataAreas = new List(); + + 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(); @@ -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(payloadJson); - if (payloadObj == null) - { - SetLastError("Token-Payload konnte nicht gelesen werden"); - return false; - } - - var granted = new HashSet(StringComparer.OrdinalIgnoreCase); - - if (payloadObj.TryGetValue("roles", out var rolesToken) && rolesToken is JArray roleArray) - { - foreach (var role in roleArray.Values()) - { - if (!string.IsNullOrWhiteSpace(role)) - granted.Add(role); - } - } - - if (!granted.Any() && payloadObj.TryGetValue("scp", out var scopeToken)) - { - var scopes = scopeToken.Value() ?? 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 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(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 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 granted, out string errorMessage) + { + granted = new HashSet(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(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()) + { + if (!string.IsNullOrWhiteSpace(role)) + granted.Add(role); + } + } + + if (!granted.Any() && payloadObj.TryGetValue("scp", out var scopeToken)) + { + var scopes = scopeToken.Value() ?? 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 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 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,