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