using C4IT.LIAM; using C4IT.Logging; using C4IT.MsGraph; using C4IT_IAM_Engine; using LiamAD; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using static C4IT.Logging.cLogManager; using static LiamAD.ADServiceGroupCreator; namespace LiamWorkflowActivities { public class GetDataAreasOperationResult { public bool Success { get; set; } public string ErrorCode { get; set; } = string.Empty; public string ErrorMessage { get; set; } = string.Empty; public List DataAreas { get; set; } = new List(); public List AutomaticEnsurePreview { get; set; } = new List(); } public class NtfsAutomaticEnsurePreviewEntry { public string FolderPath { get; set; } = string.Empty; public bool WhatIf { get; set; } = true; public string Message { get; set; } = string.Empty; public List WouldCreateGroups { get; set; } = new List(); public List WouldReuseGroups { get; set; } = new List(); public List WouldAddAclEntries { get; set; } = new List(); public List ExistingAclEntries { get; set; } = new List(); public List WouldEnsureTraverseGroups { get; set; } = new List(); public List Warnings { get; set; } = new List(); } public class GetSecurityGroupsOperationResult { public bool Success { get; set; } public string ErrorCode { get; set; } = string.Empty; public string ErrorMessage { get; set; } = string.Empty; public List SecurityGroups { get; set; } = new List(); } public class NtfsOperationResult { public bool Success { get; set; } public ResultToken ResultToken { get; set; } } public class AdServiceGroupOperationResult { public bool Success { get; set; } public string ErrorCode { get; set; } = string.Empty; public string ErrorMessage { get; set; } = string.Empty; public List> CreatedGroups { get; set; } = new List>(); } public class ExchangeProvisionOperationResult { public bool Success { get; set; } public Guid ObjectGuid { get; set; } = Guid.Empty; public List> CreatedGroups { get; set; } = new List>(); public string ErrorCode { get; set; } = string.Empty; public string ErrorMessage { get; set; } = string.Empty; } public class CloneTeamOperationResult { public bool Success { get; set; } public Guid CreatedTeamId { get; set; } = Guid.Empty; public cMsGraphResultBase Result { get; set; } public string ErrorCode { get; set; } = string.Empty; public string ErrorMessage { get; set; } = string.Empty; } public static class LiamWorkflowRuntime { public static async Task GetDataAreasFromProviderAsync(cLiamProviderBase provider, string configurationId = null, bool? simulateConfiguredNtfsPermissionEnsure = null) { var result = new GetDataAreasOperationResult(); if (provider == null) { result.ErrorCode = "WF_GET_DATAAREAS_PROVIDER_NOT_FOUND"; result.ErrorMessage = "Configured provider is not initialized."; return result; } try { var dataAreas = await provider.getDataAreasAsync(provider.MaxDepth); if (dataAreas == null) { SetErrorFromProvider(result, provider, "WF_GET_DATAAREAS_PROVIDER_CALL_FAILED", "Provider returned null while reading data areas."); return result; } var simulateAutomaticEnsure = simulateConfiguredNtfsPermissionEnsure ?? IsWorkflowWhatIfEnabled(provider); if (!await EnsureNtfsPermissionGroupsIfConfiguredAsync(provider, dataAreas, result, simulateAutomaticEnsure)) return result; result.DataAreas = dataAreas .Select(dataArea => MapDataAreaEntry(dataArea, configurationId)) .ToList(); result.Success = true; return result; } catch (Exception ex) { LogException(ex); result.ErrorCode = "WF_GET_DATAAREAS_EXCEPTION"; result.ErrorMessage = ex.Message; return result; } } public static async Task GetSecurityGroupsFromProviderAsync(cLiamProviderBase provider) { var result = new GetSecurityGroupsOperationResult(); if (provider == null) { result.ErrorCode = "WF_GET_SECURITYGROUPS_PROVIDER_NOT_FOUND"; result.ErrorMessage = "Configured provider is not initialized."; return result; } try { var securityGroups = await provider.getSecurityGroupsAsync(provider.GroupFilter); if (securityGroups == null) { SetErrorFromProvider(result, provider, "WF_GET_SECURITYGROUPS_PROVIDER_CALL_FAILED", "Provider returned null while reading security groups."); return result; } result.SecurityGroups = securityGroups .Select(MapSecurityGroupEntry) .ToList(); result.Success = true; return result; } catch (Exception ex) { LogException(ex); result.ErrorCode = "WF_GET_SECURITYGROUPS_EXCEPTION"; result.ErrorMessage = ex.Message; return result; } } public static async Task CreateDataAreaAsync( cLiamProviderBase provider, string newFolderPath, string parentFolderPath, IDictionary customTags, IEnumerable ownerSids, IEnumerable readerSids, IEnumerable writerSids) { var result = new NtfsOperationResult(); if (!(provider is cLiamProviderNtfs ntfsProvider)) { result.ResultToken = CreateInvalidNtfsResultToken("Configured provider is not NTFS or not initialized."); return result; } var token = await ntfsProvider.CreateDataAreaAsync( newFolderPath, parentFolderPath, customTags, NormalizeIdentifierList(ownerSids), NormalizeIdentifierList(readerSids), NormalizeIdentifierList(writerSids), IsWorkflowWhatIfEnabled(provider)); if (token == null) token = CreateInvalidNtfsResultToken(ntfsProvider.GetLastErrorMessage() ?? "Provider returned no result while creating the data area."); result.ResultToken = token; result.Success = token != null && token.resultErrorId == 0; return result; } public static async Task EnsureNtfsPermissionGroupsAsync( cLiamProviderBase provider, string folderPath, IDictionary customTags, IEnumerable ownerSids, IEnumerable readerSids, IEnumerable writerSids, bool ensureTraverseGroups) { var result = new NtfsOperationResult(); if (!(provider is cLiamProviderNtfs ntfsProvider) || string.IsNullOrWhiteSpace(folderPath)) { result.ResultToken = CreateInvalidNtfsResultToken(provider is cLiamProviderNtfs ? "Folder path is missing." : "Configured provider is not NTFS or not initialized."); return result; } var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "AllowManualNtfsPermissionEnsureForShares"); var token = await ntfsProvider.EnsureMissingPermissionGroupsAsync( folderPath, customTags, NormalizeIdentifierList(ownerSids), NormalizeIdentifierList(readerSids), NormalizeIdentifierList(writerSids), allowSharePathEnsure, ensureTraverseGroups, IsWorkflowWhatIfEnabled(provider)); if (token == null) token = CreateInvalidNtfsResultToken(ntfsProvider.GetLastErrorMessage() ?? "Provider returned no result while ensuring NTFS permission groups."); result.ResultToken = token; result.Success = token != null && token.resultErrorId == 0; return result; } public static AdServiceGroupOperationResult CreateAdServiceGroups( cLiamProviderBase provider, string serviceName, string description, eLiamAccessRoleScopes scope, ADGroupType groupType, IEnumerable ownerSids, IEnumerable memberSids) { var result = new AdServiceGroupOperationResult(); if (!(provider is cLiamProviderAD adProvider)) { result.ErrorCode = "WF_PROVIDER_INVALID"; result.ErrorMessage = "Configured provider is not Active Directory or not initialized."; return result; } try { var groups = adProvider.CreateServiceGroups( serviceName, description, scope, groupType, NormalizeIdentifierList(ownerSids), NormalizeIdentifierList(memberSids)); result.Success = groups != null; result.CreatedGroups = groups ?? new List>(); return result; } catch (Exception ex) { LogException(ex); result.ErrorCode = "WF_ACTIVITY_EXCEPTION"; result.ErrorMessage = ex.Message; return result; } } public static async Task CloneTeamAsync( cLiamProviderBase provider, string teamId, string name, string description, int visibility, int partsToClone, string additionalMembers, string additionalOwners) { var result = new CloneTeamOperationResult(); if (!(provider is cLiamProviderMsTeams msTeamsProvider)) { result.ErrorCode = "WF_PROVIDER_INVALID"; result.ErrorMessage = "Configured provider is not MsTeams or not initialized."; return result; } try { var cloneResult = await msTeamsProvider.cloneTeam(teamId, name, description, visibility, partsToClone, additionalMembers, additionalOwners); result.Result = cloneResult; result.Success = cloneResult != null; if (cloneResult?.Result?.targetResourceId != null) { string idString = cloneResult.Result.targetResourceId.ToString(); Guid createdTeamId; if (Guid.TryParse(idString, out createdTeamId)) { result.CreatedTeamId = createdTeamId; } else { LogEntry($"targetResourceId '{idString}' is not a valid Guid.", LogLevels.Warning); } } return result; } catch (Exception ex) { LogException(ex); result.ErrorCode = "WF_ACTIVITY_EXCEPTION"; result.ErrorMessage = ex.Message; return result; } } public static ExchangeProvisionOperationResult CreateDistributionGroup( cLiamProviderBase provider, string name, string alias, string displayName, string primarySmtpAddress) { var result = new ExchangeProvisionOperationResult(); if (!(provider is cLiamProviderExchange exchangeProvider)) { result.ErrorCode = "WF_PROVIDER_INVALID"; result.ErrorMessage = "Configured provider is not Exchange or not initialized."; return result; } try { var created = exchangeProvider.exchangeManager.CreateDistributionGroupWithOwnershipGroups( name, alias, displayName, primarySmtpAddress, out string errorCode, out string errorMessage); result.ErrorCode = errorCode ?? string.Empty; result.ErrorMessage = errorMessage ?? string.Empty; if (created != null) { result.Success = true; result.ObjectGuid = created.Item1; result.CreatedGroups = created.Item2 ?? new List>(); } return result; } catch (Exception ex) { LogException(ex); result.ErrorCode = "WF_ACTIVITY_EXCEPTION"; result.ErrorMessage = ex.Message; return result; } } public static ExchangeProvisionOperationResult CreateSharedMailbox( cLiamProviderBase provider, string name, string alias, string displayName, string primarySmtpAddress) { var result = new ExchangeProvisionOperationResult(); if (!(provider is cLiamProviderExchange exchangeProvider)) { result.ErrorCode = "WF_PROVIDER_INVALID"; result.ErrorMessage = "Configured provider is not Exchange or not initialized."; return result; } try { var created = exchangeProvider.exchangeManager.CreateSharedMailboxWithOwnershipGroups( name, alias, displayName, primarySmtpAddress, out string errorCode, out string errorMessage); result.ErrorCode = errorCode ?? string.Empty; result.ErrorMessage = errorMessage ?? string.Empty; if (created != null) { result.Success = true; result.ObjectGuid = created.Item1; result.CreatedGroups = created.Item2 ?? new List>(); } return result; } catch (Exception ex) { LogException(ex); result.ErrorCode = "WF_ACTIVITY_EXCEPTION"; result.ErrorMessage = ex.Message; return result; } } private static ResultToken CreateInvalidNtfsResultToken(string message) { return new ResultToken("LiamWorkflowRuntime") { resultErrorId = 1, resultMessage = message ?? string.Empty }; } private static IEnumerable NormalizeIdentifierList(IEnumerable identifiers) { if (identifiers == null) return Enumerable.Empty(); return identifiers .Select(i => i?.Trim()) .Where(i => !string.IsNullOrWhiteSpace(i)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); } private static async Task EnsureNtfsPermissionGroupsIfConfiguredAsync(cLiamProviderBase provider, List dataAreas, GetDataAreasOperationResult result, bool simulateOnly) { if (!(provider is cLiamProviderNtfs ntfsProvider)) return true; var allowFolderEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroups"); var allowSharePathEnsure = IsAdditionalConfigurationEnabled(provider, "EnsureNtfsPermissionGroupsForShares"); if (!allowFolderEnsure && !allowSharePathEnsure) return true; foreach (var ntfsArea in dataAreas .Where(dataArea => allowFolderEnsure && dataArea is cLiamNtfsFolder || allowSharePathEnsure && dataArea is cLiamNtfsShare) .Cast()) { var folderPath = ntfsArea.TechnicalName; if (string.IsNullOrWhiteSpace(folderPath)) continue; if (!Directory.Exists(folderPath)) { LogEntry($"Skipping automatic NTFS permission group ensure for '{folderPath}' because the directory does not exist.", LogLevels.Warning); continue; } var ensureResult = await ntfsProvider.EnsureMissingPermissionGroupsAsync( folderPath, null, null, null, null, allowSharePathEnsure, false, simulateOnly); if (ensureResult == null) { result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED"; result.ErrorMessage = $"Automatic NTFS permission group ensure failed for '{folderPath}' because the provider returned no result."; return false; } if (ensureResult.resultErrorId != 0) { result.ErrorCode = "WF_GET_DATAAREAS_ENSURE_NTFS_GROUPS_FAILED"; result.ErrorMessage = $"Automatic NTFS permission group ensure failed for '{folderPath}': {ensureResult.resultMessage}"; return false; } if (simulateOnly) { LogAutomaticNtfsEnsurePreviewDebug(folderPath, ensureResult); result.AutomaticEnsurePreview.Add(MapAutomaticEnsurePreview(folderPath, ensureResult)); continue; } LogAutomaticNtfsEnsureDebug(folderPath, ensureResult); await ntfsArea.ResolvePermissionGroupsAsync(folderPath); } return true; } private static NtfsAutomaticEnsurePreviewEntry MapAutomaticEnsurePreview(string folderPath, ResultToken ensureResult) { return new NtfsAutomaticEnsurePreviewEntry { FolderPath = folderPath ?? string.Empty, WhatIf = true, Message = ensureResult?.resultMessage ?? string.Empty, WouldCreateGroups = ensureResult?.createdGroups?.ToList() ?? new List(), WouldReuseGroups = ensureResult?.reusedGroups?.ToList() ?? new List(), WouldAddAclEntries = ensureResult?.addedAclEntries?.ToList() ?? new List(), ExistingAclEntries = ensureResult?.skippedAclEntries?.ToList() ?? new List(), WouldEnsureTraverseGroups = ensureResult?.ensuredTraverseGroups?.ToList() ?? new List(), Warnings = ensureResult?.warnings?.ToList() ?? new List() }; } private static void LogAutomaticNtfsEnsurePreviewDebug(string folderPath, ResultToken ensureResult) { if (ensureResult == null) return; LogEntry( $"Automatic NTFS permission group ensure preview finished for '{folderPath}'. " + $"WouldCreateGroups={ensureResult.createdGroups.Count}, " + $"WouldReuseGroups={ensureResult.reusedGroups.Count}, " + $"WouldAddAcls={ensureResult.addedAclEntries.Count}, " + $"ExistingAcls={ensureResult.skippedAclEntries.Count}, " + $"WouldEnsureTraverseGroups={ensureResult.ensuredTraverseGroups.Count}, " + $"Warnings={ensureResult.warnings.Count}, " + $"ResultMessage='{ensureResult.resultMessage ?? string.Empty}'", LogLevels.Debug); } private static void LogAutomaticNtfsEnsureDebug(string folderPath, ResultToken ensureResult) { if (ensureResult == null) return; LogEntry( $"Automatic NTFS permission group ensure finished for '{folderPath}'. " + $"CreatedGroups={ensureResult.createdGroups.Count}, " + $"ReusedGroups={ensureResult.reusedGroups.Count}, " + $"AddedAcls={ensureResult.addedAclEntries.Count}, " + $"SkippedAcls={ensureResult.skippedAclEntries.Count}, " + $"TraverseGroups={ensureResult.ensuredTraverseGroups.Count}, " + $"Warnings={ensureResult.warnings.Count}, " + $"ResultMessage='{ensureResult.resultMessage ?? string.Empty}'", LogLevels.Debug); if (ensureResult.createdGroups.Count > 0) { LogEntry( $"Automatic NTFS permission group ensure detected missing AD groups for '{folderPath}' and created them: {string.Join(", ", ensureResult.createdGroups)}", LogLevels.Debug); } if (ensureResult.reusedGroups.Count > 0) { LogEntry( $"Automatic NTFS permission group ensure reused existing AD groups for '{folderPath}': {string.Join(", ", ensureResult.reusedGroups)}", LogLevels.Debug); } if (ensureResult.addedAclEntries.Count > 0) { LogEntry( $"Automatic NTFS permission group ensure added missing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.addedAclEntries)}", LogLevels.Debug); } if (ensureResult.skippedAclEntries.Count > 0) { LogEntry( $"Automatic NTFS permission group ensure kept existing ACL entries for '{folderPath}': {string.Join(", ", ensureResult.skippedAclEntries)}", LogLevels.Debug); } if (ensureResult.ensuredTraverseGroups.Count > 0) { LogEntry( $"Automatic NTFS permission group ensure touched traverse groups for '{folderPath}': {string.Join(", ", ensureResult.ensuredTraverseGroups)}", LogLevels.Debug); } if (ensureResult.warnings.Count > 0) { LogEntry( $"Automatic NTFS permission group ensure produced warnings for '{folderPath}': {string.Join(" | ", ensureResult.warnings)}", LogLevels.Debug); } } private static bool IsAdditionalConfigurationEnabled(cLiamProviderBase provider, string key) { if (provider?.AdditionalConfiguration == null || string.IsNullOrWhiteSpace(key)) return false; if (!provider.AdditionalConfiguration.TryGetValue(key, out var rawValue) || string.IsNullOrWhiteSpace(rawValue)) return false; return rawValue.Equals("true", StringComparison.OrdinalIgnoreCase) || rawValue.Equals("1", StringComparison.OrdinalIgnoreCase) || rawValue.Equals("yes", StringComparison.OrdinalIgnoreCase); } private static bool IsWorkflowWhatIfEnabled(cLiamProviderBase provider) { return IsAdditionalConfigurationEnabled(provider, "WhatIf"); } private static void SetErrorFromProvider(GetDataAreasOperationResult result, cLiamProviderBase provider, string fallbackCode, string fallbackMessage) { var error = ExtractProviderError(provider, fallbackCode, fallbackMessage); result.ErrorCode = error.Item1; result.ErrorMessage = error.Item2; } private static void SetErrorFromProvider(GetSecurityGroupsOperationResult result, cLiamProviderBase provider, string fallbackCode, string fallbackMessage) { var error = ExtractProviderError(provider, fallbackCode, fallbackMessage); result.ErrorCode = error.Item1; result.ErrorMessage = error.Item2; } private static Tuple ExtractProviderError(cLiamProviderBase provider, string fallbackCode, string fallbackMessage) { if (provider is cLiamProviderExchange exchangeProvider) { var code = exchangeProvider.GetLastErrorCode(); var message = exchangeProvider.GetLastErrorMessage(); if (!string.IsNullOrWhiteSpace(code) || !string.IsNullOrWhiteSpace(message)) { return Tuple.Create( string.IsNullOrWhiteSpace(code) ? fallbackCode : code, string.IsNullOrWhiteSpace(message) ? fallbackMessage : message); } } var providerMessage = provider?.GetLastErrorMessage(); return Tuple.Create( fallbackCode, string.IsNullOrWhiteSpace(providerMessage) ? fallbackMessage : providerMessage); } private static DataAreaEntry MapDataAreaEntry(cLiamDataAreaBase dataArea, string configurationId) { var ntfsPermissionArea = dataArea as cLiamNtfsPermissionDataAreaBase; var ntfsFolder = dataArea as cLiamNtfsFolder; var adGroup = dataArea as cLiamAdGroupAsDataArea; var exchangeMailbox = dataArea as cLiamExchangeSharedMailbox; var exchangeDistribution = dataArea as cLiamExchangeDistributionGroup; var owner = exchangeMailbox?.OwnerGroupIdentifier ?? exchangeDistribution?.OwnerGroupIdentifier ?? adGroup?.ManagedBySID ?? ntfsPermissionArea?.OwnerGroupIdentifier ?? string.Empty; var write = exchangeMailbox != null ? exchangeMailbox.FullAccessGroupSid : exchangeDistribution != null ? exchangeDistribution.MemberGroupSid : adGroup?.UID ?? ntfsPermissionArea?.WriteGroupIdentifier ?? string.Empty; var read = exchangeMailbox != null ? exchangeMailbox.SendAsGroupSid : ntfsPermissionArea?.ReadGroupIdentifier ?? string.Empty; var traverse = ntfsPermissionArea?.TraverseGroupIdentifier ?? string.Empty; var created = ntfsPermissionArea?.CreatedDate ?? DateTime.MinValue.ToString("o"); var description = adGroup?.Description ?? string.Empty; return new DataAreaEntry { DisplayName = dataArea.DisplayName ?? string.Empty, UID = dataArea.UID ?? string.Empty, TechnicalName = dataArea.TechnicalName ?? string.Empty, Description = description, TargetType = ((int)dataArea.Provider.ProviderType).ToString(), ParentUID = dataArea.ParentUID ?? string.Empty, Level = dataArea.Level.ToString(), Owner = owner, Write = write, Read = read, Traverse = traverse, CreatedDate = created, ConfigurationId = configurationId ?? string.Empty, BaseFolder = ntfsFolder?.Share?.TechnicalName ?? dataArea.Provider?.RootPath ?? string.Empty, UniqueId = dataArea.UID ?? string.Empty, DataAreaType = dataArea.DataType.ToString(), DataAreaTypeId = (int)dataArea.DataType }; } private static SecurityGroupEntry MapSecurityGroupEntry(cLiamDataAreaBase securityGroup) { var entry = new SecurityGroupEntry { DisplayName = securityGroup.TechnicalName, TechnicalName = securityGroup.UID, TargetType = ((int)securityGroup.Provider.ProviderType).ToString() }; switch (securityGroup) { case cLiamAdGroup adGroup: entry.UID = adGroup.dn; entry.Scope = adGroup.scope; break; case cLiamAdGroup2 adGroup2: entry.UID = adGroup2.dn; entry.Scope = adGroup2.scope; break; case cLiamExchangeSecurityGroup exchangeGroup: entry.UID = exchangeGroup.dn; break; } return entry; } } }