<commit_before>using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Signum.Engine.Mailing;
using Signum.Engine.Scheduler;
using Signum.Entities.Authorization;
using Signum.Utilities.Reflection;
using System.Collections.Concurrent;
using System.Text.Json;

namespace Signum.Engine.Authorization;

public static class AzureADLogic
{
    public static Func<ClientCredentialProvider> GetClientCredentialProvider = () => ((ActiveDirectoryAuthorizer)AuthLogic.Authorizer!).GetConfig().GetAuthProvider();


    public static async Task<List<ActiveDirectoryUser>> FindActiveDirectoryUsers(string subStr, int top, CancellationToken token)
    {
        ClientCredentialProvider authProvider = GetClientCredentialProvider();
        GraphServiceClient graphClient = new GraphServiceClient(authProvider);

        subStr = subStr.Replace("'", "''");

        var query = subStr.Contains("@") ? $"mail eq '{subStr}'" :
            subStr.Contains(",") ? $"startswith(givenName, '{subStr.After(",").Trim()}') AND startswith(surname, '{subStr.Before(",").Trim()}') OR startswith(displayname, '{subStr.Trim()}')" :
            subStr.Contains(" ") ? $"startswith(givenName, '{subStr.Before(" ").Trim()}') AND startswith(surname, '{subStr.After(" ").Trim()}') OR startswith(displayname, '{subStr.Trim()}')" :
             $"startswith(givenName, '{subStr}') OR startswith(surname, '{subStr}') OR startswith(displayname, '{subStr.Trim()}') OR startswith(mail, '{subStr.Trim()}')";

        var result = await graphClient.Users.Request().Filter(query).Top(top).GetAsync(token);

        return result.Select(a => new ActiveDirectoryUser
        {
            UPN = a.UserPrincipalName,
            DisplayName = a.DisplayName,
            JobTitle = a.JobTitle,
            ObjectID = Guid.Parse(a.Id),
        }).ToList();
    }

    public static TimeSpan CacheADGroupsFor = new TimeSpan(0, minutes: 30, 0);

    static ConcurrentDictionary<Lite<UserEntity>, (DateTime date, List<Guid> groups)> ADGroupsCache = new ConcurrentDictionary<Lite<UserEntity>, (DateTime date, List<Guid> groups)>();

    public static List<Guid> CurrentADGroups()
    {
        var oid = UserADMixin.CurrentOID;
        if (oid == null)
            return new List<Guid>();

        var tuple = ADGroupsCache.AddOrUpdate(UserEntity.Current,
            addValueFactory: user => (Clock.Now, CurrentADGroupsInternal(oid.Value)),
            updateValueFactory: (user, old) => old.date.Add(CacheADGroupsFor) > Clock.Now ? old : (Clock.Now, CurrentADGroupsInternal(oid.Value)));

        return tuple.groups;
    }

    private static List<Guid> CurrentADGroupsInternal(Guid oid)
    {
        using (HeavyProfiler.Log("Microsoft Graph", () => "CurrentADGroups for OID: " + oid))
        {
            ClientCredentialProvider authProvider = AzureADLogic.GetClientCredentialProvider();
            GraphServiceClient graphClient = new GraphServiceClient(authProvider);
            var result = graphClient.Users[oid.ToString()].MemberOf.WithODataCast("microsoft.graph.group").Request().Top(999).Select("id, displayName, ODataType").GetAsync().Result.ToList();

            return result.Select(a => Guid.Parse(a.Id)).ToList();
        }
    }

    public static void Start(SchemaBuilder sb, bool adGroups, bool deactivateUsersTask)
    {
        if (sb.NotDefined(MethodInfo.GetCurrentMethod()))
        {
            if (deactivateUsersTask)
            {
                SimpleTaskLogic.Register(ActiveDirectoryTask.DeactivateUsers, stc =>
                {
                    var list = Database.Query<UserEntity>().Where(u => u.Mixin<UserADMixin>().OID != null).ToList();

                    ClientCredentialProvider authProvider = GetClientCredentialProvider();
                    GraphServiceClient graphClient = new GraphServiceClient(authProvider);
                    stc.ForeachWriting(list.Chunk(10), gr => gr.Length + " user(s)...", gr =>
                    {
                        var filter = gr.Select(a => "id eq '" + a.Mixin<UserADMixin>().OID + "'").Combined(FilterGroupOperation.Or);
                        var users = graphClient.Users.Request().Filter(filter).Select("accountEnabled, id").GetAsync().Result;

                        var isEnabledDictionary = users.ToDictionary(a => Guid.Parse(a.Id), a => a.AccountEnabled!.Value);

                        foreach (var u in gr)
                        {
                            if (u.State == UserState.Active && !isEnabledDictionary.GetOrThrow(u.Mixin<UserADMixin>().OID!.Value))
                            {
                                stc.StringBuilder.AppendLine($"User {u.Id} ({u.UserName}) with OID {u.Mixin<UserADMixin>().OID} has been deactivated in Azure AD");
                                u.Execute(UserOperation.Deactivate);
                            }
                        }
                    });

                    return null;
                });
            }

            if (adGroups)
            {
                sb.Include<ADGroupEntity>()
                    .WithQuery(() => e => new
                    {
                        Entity = e,
                        e.Id,
                        e.DisplayName
                    });

                Schema.Current.OnMetadataInvalidated += () => ADGroupsCache.Clear();

                new Graph<ADGroupEntity>.Execute(ADGroupOperation.Save)
                {
                    CanBeNew = true,
                    CanBeModified = true,
                    Execute = (e, _) =>
                    {
                        if (e.IsNew && e.IdOrNull != null)
                            Administrator.SaveDisableIdentity(e);
                    },
                }.Register();

                new Graph<ADGroupEntity>.Delete(ADGroupOperation.Delete)
                {
                    Delete = (e, _) => e.Delete(),
                }.Register();


            

                QueryLogic.Queries.Register(UserADQuery.ActiveDirectoryUsers, () => DynamicQueryCore.Manual(async (request, queryDescription, cancellationToken) =>
                 {
                     using (HeavyProfiler.Log("Microsoft Graph", () => "ActiveDirectoryUsers"))
                     {
                         ClientCredentialProvider authProvider = GetClientCredentialProvider();
                         GraphServiceClient graphClient = new GraphServiceClient(authProvider);

                         var inGroup = (FilterCondition?)request.Filters.Extract(f => f is FilterCondition fc && fc.Token.Key == "InGroup" && fc.Operation == FilterOperation.EqualTo).SingleOrDefaultEx();

                         var query = graphClient.Users.Request()
                            .InGroup(inGroup?.Value as Lite<ADGroupEntity>)
                            .Filter(request.Filters)
                            .Search(request.Filters)
                            .Select(request.Columns)
                            .OrderBy(request.Orders)
                            .Paginate(request.Pagination);

                         query.QueryOptions.Add(new QueryOption("$count", "true"));
                         query.Headers.Add(new HeaderOption("ConsistencyLevel", "eventual"));

                         var result = await query.GetAsync(cancellationToken);

                         var count = ((JsonElement)result.AdditionalData["@odata.count"]).GetInt32();

                         var skip = request.Pagination is Pagination.Paginate p ? (p.CurrentPage - 1) * p.ElementsPerPage : 0;

                         return result.Skip(skip).Select(u => new
                         {
                             Entity = (Lite<Entities.Entity>?)null,
                             u.Id,
                             u.DisplayName,
                             u.UserPrincipalName,
                             u.Mail,
                             u.GivenName,
                             u.Surname,
                             u.JobTitle,
                             OnPremisesExtensionAttributes = u.OnPremisesExtensionAttributes?.Let(ea => new OnPremisesExtensionAttributesModel
                             {
                                 ExtensionAttribute1 = ea.ExtensionAttribute1,
                                 ExtensionAttribute2 = ea.ExtensionAttribute2,
                                 ExtensionAttribute3 = ea.ExtensionAttribute3,
                                 ExtensionAttribute4 = ea.ExtensionAttribute4,
                                 ExtensionAttribute5 = ea.ExtensionAttribute5,
                                 ExtensionAttribute6 = ea.ExtensionAttribute6,
                                 ExtensionAttribute7 = ea.ExtensionAttribute7,
                                 ExtensionAttribute8 = ea.ExtensionAttribute8,
                                 ExtensionAttribute9 = ea.ExtensionAttribute9,
                                 ExtensionAttribute10 = ea.ExtensionAttribute10,
                                 ExtensionAttribute11 = ea.ExtensionAttribute11,
                                 ExtensionAttribute12 = ea.ExtensionAttribute12,<commit_msg>add fields to ActiveDirectoryUsers<commit_after>using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Signum.Engine.Mailing;
using Signum.Engine.Scheduler;
using Signum.Entities.Authorization;
using Signum.Utilities.Reflection;
using System.Collections.Concurrent;
using System.Text.Json;

namespace Signum.Engine.Authorization;

public static class AzureADLogic
{
    public static Func<ClientCredentialProvider> GetClientCredentialProvider = () => ((ActiveDirectoryAuthorizer)AuthLogic.Authorizer!).GetConfig().GetAuthProvider();


    public static async Task<List<ActiveDirectoryUser>> FindActiveDirectoryUsers(string subStr, int top, CancellationToken token)
    {
        ClientCredentialProvider authProvider = GetClientCredentialProvider();
        GraphServiceClient graphClient = new GraphServiceClient(authProvider);

        subStr = subStr.Replace("'", "''");

        var query = subStr.Contains("@") ? $"mail eq '{subStr}'" :
            subStr.Contains(",") ? $"startswith(givenName, '{subStr.After(",").Trim()}') AND startswith(surname, '{subStr.Before(",").Trim()}') OR startswith(displayname, '{subStr.Trim()}')" :
            subStr.Contains(" ") ? $"startswith(givenName, '{subStr.Before(" ").Trim()}') AND startswith(surname, '{subStr.After(" ").Trim()}') OR startswith(displayname, '{subStr.Trim()}')" :
             $"startswith(givenName, '{subStr}') OR startswith(surname, '{subStr}') OR startswith(displayname, '{subStr.Trim()}') OR startswith(mail, '{subStr.Trim()}')";

        var result = await graphClient.Users.Request().Filter(query).Top(top).GetAsync(token);

        return result.Select(a => new ActiveDirectoryUser
        {
            UPN = a.UserPrincipalName,
            DisplayName = a.DisplayName,
            JobTitle = a.JobTitle,
            ObjectID = Guid.Parse(a.Id),
        }).ToList();
    }

    public static TimeSpan CacheADGroupsFor = new TimeSpan(0, minutes: 30, 0);

    static ConcurrentDictionary<Lite<UserEntity>, (DateTime date, List<Guid> groups)> ADGroupsCache = new ConcurrentDictionary<Lite<UserEntity>, (DateTime date, List<Guid> groups)>();

    public static List<Guid> CurrentADGroups()
    {
        var oid = UserADMixin.CurrentOID;
        if (oid == null)
            return new List<Guid>();

        var tuple = ADGroupsCache.AddOrUpdate(UserEntity.Current,
            addValueFactory: user => (Clock.Now, CurrentADGroupsInternal(oid.Value)),
            updateValueFactory: (user, old) => old.date.Add(CacheADGroupsFor) > Clock.Now ? old : (Clock.Now, CurrentADGroupsInternal(oid.Value)));

        return tuple.groups;
    }

    private static List<Guid> CurrentADGroupsInternal(Guid oid)
    {
        using (HeavyProfiler.Log("Microsoft Graph", () => "CurrentADGroups for OID: " + oid))
        {
            ClientCredentialProvider authProvider = AzureADLogic.GetClientCredentialProvider();
            GraphServiceClient graphClient = new GraphServiceClient(authProvider);
            var result = graphClient.Users[oid.ToString()].MemberOf.WithODataCast("microsoft.graph.group").Request().Top(999).Select("id, displayName, ODataType").GetAsync().Result.ToList();

            return result.Select(a => Guid.Parse(a.Id)).ToList();
        }
    }

    public static void Start(SchemaBuilder sb, bool adGroups, bool deactivateUsersTask)
    {
        if (sb.NotDefined(MethodInfo.GetCurrentMethod()))
        {
            if (deactivateUsersTask)
            {
                SimpleTaskLogic.Register(ActiveDirectoryTask.DeactivateUsers, stc =>
                {
                    var list = Database.Query<UserEntity>().Where(u => u.Mixin<UserADMixin>().OID != null).ToList();

                    ClientCredentialProvider authProvider = GetClientCredentialProvider();
                    GraphServiceClient graphClient = new GraphServiceClient(authProvider);
                    stc.ForeachWriting(list.Chunk(10), gr => gr.Length + " user(s)...", gr =>
                    {
                        var filter = gr.Select(a => "id eq '" + a.Mixin<UserADMixin>().OID + "'").Combined(FilterGroupOperation.Or);
                        var users = graphClient.Users.Request().Filter(filter).Select("accountEnabled, id").GetAsync().Result;

                        var isEnabledDictionary = users.ToDictionary(a => Guid.Parse(a.Id), a => a.AccountEnabled!.Value);

                        foreach (var u in gr)
                        {
                            if (u.State == UserState.Active && !isEnabledDictionary.GetOrThrow(u.Mixin<UserADMixin>().OID!.Value))
                            {
                                stc.StringBuilder.AppendLine($"User {u.Id} ({u.UserName}) with OID {u.Mixin<UserADMixin>().OID} has been deactivated in Azure AD");
                                u.Execute(UserOperation.Deactivate);
                            }
                        }
                    });

                    return null;
                });
            }

            if (adGroups)
            {
                sb.Include<ADGroupEntity>()
                    .WithQuery(() => e => new
                    {
                        Entity = e,
                        e.Id,
                        e.DisplayName
                    });

                Schema.Current.OnMetadataInvalidated += () => ADGroupsCache.Clear();

                new Graph<ADGroupEntity>.Execute(ADGroupOperation.Save)
                {
                    CanBeNew = true,
                    CanBeModified = true,
                    Execute = (e, _) =>
                    {
                        if (e.IsNew && e.IdOrNull != null)
                            Administrator.SaveDisableIdentity(e);
                    },
                }.Register();

                new Graph<ADGroupEntity>.Delete(ADGroupOperation.Delete)
                {
                    Delete = (e, _) => e.Delete(),
                }.Register();


            

                QueryLogic.Queries.Register(UserADQuery.ActiveDirectoryUsers, () => DynamicQueryCore.Manual(async (request, queryDescription, cancellationToken) =>
                 {
                     using (HeavyProfiler.Log("Microsoft Graph", () => "ActiveDirectoryUsers"))
                     {
                         ClientCredentialProvider authProvider = GetClientCredentialProvider();
                         GraphServiceClient graphClient = new GraphServiceClient(authProvider);

                         var inGroup = (FilterCondition?)request.Filters.Extract(f => f is FilterCondition fc && fc.Token.Key == "InGroup" && fc.Operation == FilterOperation.EqualTo).SingleOrDefaultEx();

                         var query = graphClient.Users.Request()
                            .InGroup(inGroup?.Value as Lite<ADGroupEntity>)
                            .Filter(request.Filters)
                            .Search(request.Filters)
                            .Select(request.Columns)
                            .OrderBy(request.Orders)
                            .Paginate(request.Pagination);

                         query.QueryOptions.Add(new QueryOption("$count", "true"));
                         query.Headers.Add(new HeaderOption("ConsistencyLevel", "eventual"));

                         var result = await query.GetAsync(cancellationToken);

                         var count = ((JsonElement)result.AdditionalData["@odata.count"]).GetInt32();

                         var skip = request.Pagination is Pagination.Paginate p ? (p.CurrentPage - 1) * p.ElementsPerPage : 0;

                         return result.Skip(skip).Select(u => new
                         {
                             Entity = (Lite<Entities.Entity>?)null,
                             u.Id,
                             u.DisplayName,
                             u.UserPrincipalName,
                             u.Mail,
                             u.GivenName,
                             u.Surname,
                             u.JobTitle,
                             u.Department,
                             u.OfficeLocation,
                             u.EmployeeType,
                             OnPremisesExtensionAttributes = u.OnPremisesExtensionAttributes?.Let(ea => new OnPremisesExtensionAttributesModel
                             {
                                 ExtensionAttribute1 = ea.ExtensionAttribute1,
                                 ExtensionAttribute2 = ea.ExtensionAttribute2,
                                 ExtensionAttribute3 = ea.ExtensionAttribute3,
                                 ExtensionAttribute4 = ea.ExtensionAttribute4,
                                 ExtensionAttribute5 = ea.ExtensionAttribute5,
                                 ExtensionAttribute6 = ea.ExtensionAttribute6,
                                 ExtensionAttribute7 = ea.ExtensionAttribute7,
                                 ExtensionAttribute8 = ea.ExtensionAttribute8,
                                 ExtensionAttribute9 = ea.ExtensionAttribute9,
                                 ExtensionAttribute10 = ea.ExtensionAttribute10,
                                 ExtensionAttribute11 = ea.ExtensionAttribute11,
                                 ExtensionAttribute12 = ea.ExtensionAttribute12,
{'fraction_non_alphanumeric': 0.10728303532490188, 'fraction_numerical': 0.004688181421718273, 'mean_word_length': 1.7262594739188586, 'pattern_counts': {'":': 0, '<': 47, '<?xml version=': 0, '>': 97, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 24, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4819251', 'n_tokens_mistral': 4604, 'n_tokens_neox': 4388, 'n_words': 775}