Сохранять сущности в базе данных вместо appsettings.json

У меня есть проект, где я использую IdentityServer4 и PolicyServer.Local. У IdentityServer4 уже есть реализация для хранения необходимых данных в базе данных, но PolicyServer нет.

Поэтому я попытался реализовать это сам, но с успехом, но это не очень хорошо в том смысле, что я думаю, что заменяю большую часть кода PolicyServers.

Как, например, я заменил все классы Entity PolicyServers (Policy, Permission, Roles) и добавил свои собственные, чтобы я мог разрешить свойства List, и все это потому, что Entity Framework не может отобразить List в основном.

Я также добавил свой собственный PolicyServerRuntimeClient, потому что мне нужно было настроить Evaluate-Methods для новых Entity-классов.

Первый из моих Startup.cs:

        services.AddDbContext<AuthorizeDbContext>(builder => 
            builder.UseSqlite(csAuthorizeContext, sqlOptions =>
                sqlOptions.MigrationsAssembly(migrationsAssembly)));

        services.AddScoped<IAuthorizeService, AuthorizeService>()
            .AddTransient<IPolicyServerRuntimeClient, CustomPolicyServerRuntimeClient>()
            .AddScoped(provider => provider.GetRequiredService<IOptionsSnapshot<Policy>>().Value);
        new PolicyServerBuilder(services).AddAuthorizationPermissionPolicies();

(AuthorizeService для получения значений из базы данных)

Например, это мои Permission-, Roles- и для разрешения mn-отношения PermissionRoles-classes.

public class Permission
{
    [Key]
    public string Id { get; set; }

    [Required]
    public string Name { get; set; }

    [Required]
    [ForeignKey("Policy")]
    public string PolicyId { get; set; }

    public IList<PermissionRole> PermissionRoles { get; set; }
}

public class PermissionRole
{
    [Key]
    public string Id { get; set; }

    [Required]
    public string PermissionId { get; set; }

    public Permission Permission { get; set; }

    [Required]
    public string RoleId { get; set; }

    public Role Role { get; set; }
}

public class Role
{
    [Key]
    public string Id { get; set; }

    [Required]
    public string Name { get; set; }

    public IList<PermissionRole> PermissionRoles { get; set; }
}

и это были бы мои Evalute-методы в CustomPolicyServerRuntimeClient:

    public async Task<PolicyResult> EvaluateAsync(ClaimsPrincipal user)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));

        var sub = user.FindFirst("sub")?.Value;

        if (String.IsNullOrWhiteSpace(sub))
            return null;

        var roles =  _auth.Roles
            .ToList()
            .Where(x => EvaluateRole(x, user))
            .Select(x => x.Name)
            .ToArray();

        var permissions = _auth.Permissions
            .ToList()
            .Where(x => EvaluatePermission(x, roles))
            .Select(x => x.Name)
            .ToArray();

        var result = new PolicyResult()
        {
            Roles = roles.Distinct(),
            Permissions = permissions.Distinct()
        };

        return await Task.FromResult(result);
    }

    internal bool EvaluateRole(Role role, ClaimsPrincipal user)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));

        var subClaim = user.FindFirst("sub")?.Value;

        var subjectsOfDbRole = _auth.UserDetails
            .ToList()
            .Where(x => x.RoleId.Equals(role.Id))
            .Select(x => x.Subject)
            .ToList();

        return subjectsOfDbRole.Contains(subClaim);
    }

    public bool EvaluatePermission(Permission permission, IEnumerable<string> roles)
    {
        if (roles == null)
            throw new ArgumentNullException(nameof(roles));

        var permissionRoles = _auth.PermissionRoles
            .ToList()
            .Where(y => y.PermissionId.Equals(permission.Id))
            .ToList();

        if (permissionRoles.Any(x => roles.Contains(x.Role.Name)))
            return true;

        return false;
    }

это основные изменения, которые я сделал, чтобы заставить его работать.

Я не хочу делать много работы в Бэкэнде, прежде чем я пойму, как сделать это правильно.

Ожидаемый результат состоял в том, что мне, вероятно, просто нужно было заменить

        services.Configure<Policy>(configuration);

но в конце концов я заменил путь больше, чем ожидалось.

1 ответ

Решение

Вам не нужно ничего менять в PolicyServer, просто добавьте нового поставщика конфигурации, который возвращает нужные вам параметры. PolicyServer считывает свою конфигурацию из инфраструктуры конфигурации.NET Core. Это не связано с appsettings.json,

.NET Core может считывать конфигурацию из любого источника через провайдеров. Эти провайдеры не делают ничего сложного, они "просто" читают любой фактический источник и создают пары ключ / значение в виде:

"array:entries:0"= "value0"
"array:entries:1"= "value1"
"array:entries:2"= "value2"
"array:entries:4"= "value4"
"array:entries:5"= "value5"

appsettings.json не имеет особого значения, это просто файл JSON, из которого поставщик конфигурации JSON.NET Core считывает настройки ключ / значение. Файл может быть назван как угодно. Эти же данные могут быть загружены из словаря, базы данных, службы удаленной настройки и т. Д.

Этот словарь например:

public static Dictionary<string, string> arrayDict = new Dictionary<string, string>
    {
        {"array:entries:0", "value0"},
        {"array:entries:1", "value1"},
        {"array:entries:2", "value2"},
        {"array:entries:4", "value4"},
        {"array:entries:5", "value5"}
    };

Предоставляет те же данные конфигурации, что и этот файл JSON:

{
    "array" : {
        "entries" : [
            "value1",
            "value2",
            "value3",
            "value4",
            "value5"
        ]
    }
}

Использование словаря

Вы можете загрузить настройки PolicyServer из словаря с помощью поставщика конфигурации памяти. В вашем разделе конфигурации:

public static readonly Dictionary<string, string> _dict = 
    new Dictionary<string, string>
    {
        {"Policy:roles:0:name", "doctor"},
        {"Policy:roles:0:subjects:0", "1"},
        {"Policy:roles:0:subjects:1", "2"},
        {"Policy:roles:1:name", "patient"},
        {"Policy:roles:1:identityRoles:0", "customer"},
    };

public static void Main(string[] args)
{
    CreateWebHostBuilder(args).Build().Run();
}

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
    WebHost.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.AddInMemoryCollection(_dict);
        })
        .UseStartup<Startup>();

Когда вы звоните AddPolicyServerClient(Configuration.GetSection("Policy")) в регистрационном коде вашей службы настройки будут исходить из этого словаря.

Использование необработанной таблицы

Вы можете создать свой собственный поставщик конфигурации, как показано в Custom Provider, который извлекал параметры из ID/Value Таблица. Вы должны хранить полный ключ в ID поле, которое может быть немного раздражающим, например:

CREATE TABLE MyPolicySettings (ID varchar(200) PRIMARY KEY,value varchar(200))
INSERT INTO TABLE MyPolicySettings (ID,Value)
VALUES
("Policy:roles:0:name",            "doctor"},
("Policy:roles:0:subjects:0",      "1"),
("Policy:roles:0:subjects:1",      "2"),
("Policy:roles:1:name",            "patient"),
("Policy:roles:1:identityRoles:0", "customer");

Используя EF

Другой вариант - сохранить настройки в соответствующих таблицах, например, Roles, Subjects, IdentityRoles и использовать ORM для загрузки всей структуры. Как только он у вас появится, вам придется воспроизвести структуру ключа, например, перебирая объекты в итераторе:

public IEnumerable<KeyValuePair<string,string>> FlattenRoles(IEnumerable<MyRole> roles)
{
    int iRole=0;
    foreach(var role in roles)
    {           
        var rolePart=$"Policy:roles:{i}";
        var namePair=new KeyValuePair($"{rolePart}:name",role.Name);
        yield return namePair;
        int iSubject=0;
        foreach(var subjectPair in FlattenSubjects(role.Subject))
        {
            yield return subjectPair
        }
        //Same for identity roles etc
        iRole++;
    }
}

public IEnumerable<KeyValuePair<string,string>> FlattenSubjects(IEnumerable<MySubject> subjects,string rolePart)
{
    var pairs=subjects.Select((subject,idx)=>
              new KeyValuePair($"{rolePart}:subjects:{idx}",subject.Value);
    return pairs;
}

Ваш поставщик пользовательских настроек может использовать это для загрузки строго типизированных классов из базы данных, их выравнивания и преобразования в словарь, например:

public class MyPolicyConfigurationProvider: ConfigurationProvider
{
    public MyPolicyConfigurationProvider(Action<DbContextOptionsBuilder> optionsAction)
    {
        OptionsAction = optionsAction;
    }

    Action<DbContextOptionsBuilder> OptionsAction { get; }

    // Load config data from EF DB.
    public override void Load()
    {
        var builder = new DbContextOptionsBuilder<MyPoliciesContext>();

        OptionsAction(builder);

        using (var dbContext = new MyPoliciesContext(builder.Options))
        {
            var keys=FlattenRoles(dbContext.Roles);
            Data=new Dictionary<string,string>(keys);
        }
    }
}
Другие вопросы по тегам