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