Created
March 27, 2012 11:58
-
-
Save PaulStovell/2215304 to your computer and use it in GitHub Desktop.
A permissions model
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections.Generic; | |
using System.Linq; | |
using NUnit.Framework; | |
namespace Auth | |
{ | |
[TestFixture] | |
public class RuleSetFixture | |
{ | |
private RuleSet rules; | |
[Test] | |
public void ShouldAllowAdminsToDeployToProduction() | |
{ | |
Rules( | |
Allow.UsersIn("Admins").To("deploy-release").For("environments/production") | |
); | |
IsGranted(WhenIAm.In("Power Users", "Admins").AndIWantTo("deploy-release").Regarding("environments/production")); | |
} | |
[Test] | |
public void ShouldAllowAnyoneToDeployToUatButOnlyAdminsToProduction() | |
{ | |
Rules( | |
Allow.UsersIn("Admins").To("deploy-release").For("environments/production"), | |
Allow.UsersIn("Everyone").To("deploy-release").For("environments/uat")); | |
IsGranted(WhenIAm.In("Everyone").AndIWantTo("deploy-release").Regarding("environments/uat")); | |
IsDenied(WhenIAm.In("Everyone").AndIWantTo("deploy-release").Regarding("environments/production")); | |
IsGranted(WhenIAm.In("Everyone", "Admins").AndIWantTo("deploy-release").Regarding("environments/uat")); | |
IsGranted(WhenIAm.In("Everyone", "Admins").AndIWantTo("deploy-release").Regarding("environments/production")); | |
} | |
[Test] | |
public void ShouldOnlyAllowTeamMembersToDeploy() | |
{ | |
Rules( | |
Allow.UsersIn("Releasers").To("deploy-release").For("environments/production"), | |
Deny.UsersIn("TeamA").To("deploy-release").For("project/1").And("environments/production"), | |
Allow.UsersIn("TeamA").To("deploy-release").For("project/1"), | |
Allow.UsersIn("TeamB").To("deploy-release").For("environments/uat")); | |
IsDenied(WhenIAm.In("TeamA").AndIWantTo("deploy-release").Regarding("environments/production")); | |
IsGranted(WhenIAm.In("Releasers").AndIWantTo("deploy-release").Regarding("environments/production")); | |
IsGranted(WhenIAm.In("TeamA", "Releasers").AndIWantTo("deploy-release").Regarding("environments/production")); | |
IsGranted(WhenIAm.In("TeamA").AndIWantTo("deploy-release").Regarding("project/1")); | |
IsGranted(WhenIAm.In("TeamA").AndIWantTo("deploy-release").Regarding("project/1", "environments/uat")); | |
IsDenied(WhenIAm.In("TeamA").AndIWantTo("deploy-release").Regarding("project/1", "environments/production")); | |
IsDenied(WhenIAm.In("TeamB").AndIWantTo("deploy-release").Regarding("project/1")); | |
} | |
[Test] | |
public void ShouldApplyRulesBasedOnOrder() | |
{ | |
Rules( | |
Deny.UsersIn("TeamA").To("deploy-release").For("project/1").And("environments/production"), | |
Allow.UsersIn("Releasers").To("deploy-release").For("environments/production")); | |
// This user is in both groups, but since the 'deny' rule is hit first, it is denied | |
IsDenied(WhenIAm.In("TeamA", "Releasers").AndIWantTo("deploy-release").Regarding("environments/production", "project/1")); | |
Rules( | |
Allow.UsersIn("Releasers").To("deploy-release").For("environments/production"), | |
Deny.UsersIn("TeamA").To("deploy-release").For("project/1").And("environments/production")); | |
// With the rules in a different order, he now has access | |
IsGranted(WhenIAm.In("TeamA", "Releasers").AndIWantTo("deploy-release").Regarding("environments/production")); | |
} | |
[Test] | |
public void ShouldDenyWhenNoRulesApply() | |
{ | |
Rules( | |
Allow.UsersIn("Releasers").To("deploy-release").For("environments/production")); | |
IsDenied(WhenIAm.In("Releasers").AndIWantTo("do-something-else").Regarding("environments/production", "project/1")); | |
IsDenied(WhenIAm.In("Releasers").AndIWantTo("deploy-release").Regarding("environments/uat")); | |
IsDenied(WhenIAm.In("Unrecognised").AndIWantTo("deploy-release").Regarding("anything")); | |
} | |
protected void Rules(params RuleBuilder[] ruleBuilders) | |
{ | |
rules = new RuleSet(ruleBuilders.Select(r => r.Build()).ToArray()); | |
} | |
protected void IsGranted(AuthorizationRequest request) | |
{ | |
Assert.IsTrue(rules.IsAllowed(request)); | |
} | |
protected void IsDenied(AuthorizationRequest request) | |
{ | |
Assert.IsFalse(rules.IsAllowed(request)); | |
} | |
} | |
public static class Allow | |
{ | |
public static PartialRuleBuilder UsersIn(string group) | |
{ | |
return new PartialRuleBuilder(true, group); | |
} | |
} | |
public static class Deny | |
{ | |
public static PartialRuleBuilder UsersIn(string group) | |
{ | |
return new PartialRuleBuilder(false, group); | |
} | |
} | |
public class PartialRuleBuilder | |
{ | |
private readonly bool allow; | |
private readonly string groupName; | |
public PartialRuleBuilder(bool allow, string groupName) | |
{ | |
this.allow = allow; | |
this.groupName = groupName; | |
} | |
public RuleBuilder To(string operation) | |
{ | |
return new RuleBuilder(allow, groupName, operation); | |
} | |
} | |
public class RuleBuilder | |
{ | |
private readonly bool allow; | |
private readonly string groupName; | |
private readonly string operation; | |
private readonly List<string> documentIds = new List<string>(); | |
public RuleBuilder(bool allow, string groupName, string operation) | |
{ | |
this.allow = allow; | |
this.groupName = groupName; | |
this.operation = operation; | |
} | |
public RuleBuilder For(string documentId) | |
{ | |
documentIds.Add(documentId); | |
return this; | |
} | |
public RuleBuilder And(string documentId) | |
{ | |
documentIds.Add(documentId); | |
return this; | |
} | |
public Rule Build() | |
{ | |
return new Rule(allow, groupName, operation, documentIds.ToArray()); | |
} | |
} | |
public class RuleSet | |
{ | |
private readonly Rule[] rules; | |
public RuleSet(params Rule[] rules) | |
{ | |
this.rules = rules; | |
} | |
public bool IsAllowed(AuthorizationRequest request) | |
{ | |
foreach (var rule in rules.Where(rule => rule.Operation == request.Operation && request.Groups.Contains(rule.GroupId))) | |
{ | |
var notMet = rule.DocumentIds.Except(request.Documents).ToList(); | |
if (notMet.Count == 0) | |
{ | |
return rule.Allow; | |
} | |
} | |
return false; | |
} | |
} | |
public static class WhenIAm | |
{ | |
public static AuthorizationRequest In(params string[] groups) | |
{ | |
return new AuthorizationRequest(groups); | |
} | |
} | |
public class AuthorizationRequest | |
{ | |
private readonly string[] groups; | |
private string operation; | |
private string[] documents; | |
public AuthorizationRequest(string[] groups) | |
{ | |
this.groups = groups; | |
} | |
public string[] Groups | |
{ | |
get { return groups; } | |
} | |
public string Operation | |
{ | |
get { return operation; } | |
} | |
public string[] Documents | |
{ | |
get { return documents; } | |
} | |
public AuthorizationRequest AndIWantTo(string operation) | |
{ | |
this.operation = operation; | |
return this; | |
} | |
public AuthorizationRequest Regarding(params string[] documents) | |
{ | |
this.documents = documents; | |
return this; | |
} | |
} | |
public class Rule | |
{ | |
private readonly bool allow; | |
private readonly string groupId; | |
private readonly string operation; | |
private readonly string[] documentIds; | |
public Rule(bool allow, string groupId, string operation, string[] documentIds) | |
{ | |
this.allow = allow; | |
this.groupId = groupId; | |
this.operation = operation; | |
this.documentIds = documentIds; | |
} | |
public bool Allow | |
{ | |
get { return allow; } | |
} | |
public string GroupId | |
{ | |
get { return groupId; } | |
} | |
public string Operation | |
{ | |
get { return operation; } | |
} | |
public string[] DocumentIds | |
{ | |
get { return documentIds; } | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment