Search code examples
c#policy-server

Persist Entitys in Database instead of appsettings.json


I have an Project where I user IdentityServer4 and PolicyServer.Local. IdentityServer4 already has an implementation for storing the necessary data in a database, but PolicyServer doesnt.

So i tried to implement it myself, with success, but it feels not good in the sense that i think iam replacing to much of the PolicyServers Code.

Like for example I have replaced all of the PolicyServers Entity classes (Policy, Permission, Roles) and added my own ones so I can resolve the List Properties, all that because Entity Framework cant map List basically.

I also added my own PolicyServerRuntimeClient, because I needed to adjust the Evaluate-Methods to the new Entity-classes.

First of my 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 is for getting the values from the Database)

For Example this is my Permission-, Roles- and to resolve the m-n relationship a 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; }
}

and this would be my Evalute-Methods in the 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;
    }

these are the main changes I did to get it working.

I dont want to do to much work in the Backend before I figure out how to do this correctly.

Expected result was that I probably just needed to replace

        services.Configure<Policy>(configuration);

but in the end I did replace way more than expected.


Solution

  • You shouldn't have to change anything in PolicyServer, just add a new configuration provider that returns the settings you want. PolicyServer reads its configuration from .NET Core's configuration infrastructure. It's not tied to appsettings.json.

    .NET Core can read configuration from any source through providers. Those providers don't do anything complicated, they "just" read whatever their actual source is and produce key/value string pairs in the form:

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

    appsettings.json has no special meaning, it's just a JSON file from which .NET Core's JSON configuration provider reads key/value settings. The file can be named anything at all. The same data could be loaded from a dictionary, a database, a remote configuration service etc.

    This dictionary for example :

    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"}
        };
    

    Provides the same configuration data as this JSON file :

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

    Using a dictionary

    You could load PolicyServer's settings from a dictionary using the Memory configuration provider. In your configuration section, :

    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>();
    

    When you call AddPolicyServerClient(Configuration.GetSection("Policy")) in your service registration code, the settings will come from that dictionary.

    Using a raw table

    You could create your own configuration provider as shown in Custom Configuration Provider that retrieved settings from an ID/Value table. You'd have to store the full key in the ID field, which can be a bit annoying, eg :

    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");
    

    Using EF

    Another option is to store your settings in proper tables eg, Roles, Subjects, IdentityRoles and use an ORM to load the entire structure. Once you have it, you'll have to reproduce the key structure, eg by iterating over the objects in an iterator :

    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;
    }
    

    Your custom configuration provider could use this to load strongly-typed classes from the database, flatten them and convert them to a dictionary, eg :

    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);
            }
        }
    }