[BREAKING] Flatten the ACL rules to enable some use cases.

With previous configuration format rules were not ordered between groups and
thus not predictable. Also in some cases `any` must have been a higher
precedence than `groups`. Flattening the rules let the user apply whatever
policy he can think of.

When several rules match the (subject, domain, resource), the first one is
applied.

NOTE: This commit changed the format for declaring ACLs. Be sure to update
your configuration file before upgrading.
This commit is contained in:
Clement Michaud 2018-10-24 00:29:09 +02:00 committed by Clement Michaud
parent 2bc650fd97
commit 97bfafb6eb
19 changed files with 317 additions and 348 deletions

View File

@ -29,37 +29,37 @@ totp:
access_control: access_control:
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
default_policy: deny default_policy: deny
any:
rules:
- domain: single_factor.example.com - domain: single_factor.example.com
policy: one_factor policy: one_factor
groups:
admins:
# All resources in all domains
- domain: '*.example.com'
policy: two_factor
# Except mx2.mail.example.com (it restricts the first rule)
#- domain: 'mx2.mail.example.com'
# policy: deny
# User-based rules. - domain: '*.example.com'
users: subject: "group:admins"
john:
- domain: dev.example.com
policy: two_factor policy: two_factor
- domain: dev.example.com
resources: resources:
- '^/users/john/.*$' - '^/users/john/.*$'
harry: subject: "user:john"
- domain: dev.example.com
policy: two_factor policy: two_factor
- domain: dev.example.com
resources: resources:
- '^/users/harry/.*$' - '^/users/harry/.*$'
bob: subject: "user:harry"
policy: two_factor
- domain: '*.mail.example.com' - domain: '*.mail.example.com'
subject: "user:bob"
policy: two_factor policy: two_factor
- domain: 'dev.example.com'
policy: two_factor - domain: dev.example.com
resources: resources:
- '^/users/bob/.*$' - '^/users/bob/.*$'
subject: "user:bob"
policy: two_factor
# Configuration of the authentication regulation mechanism. # Configuration of the authentication regulation mechanism.
regulation: regulation:

View File

@ -86,108 +86,92 @@ authentication_backend:
## path: ./users_database.yml ## path: ./users_database.yml
# Authentication methods
#
# Authentication methods can be defined per subdomain.
# There are currently two available methods: "single_factor" and "two_factor"
#
# Note: by default a domain uses "two_factor" method.
#
# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and
# values must be one of the two possible methods.
#
# Note: 'per_subdomain_methods' is optional.
#
# Note: authentication_methods is optional. If it is not set all sub-domains
# are protected by two factors.
authentication_methods:
default_method: two_factor
per_subdomain_methods:
# Access Control # Access Control
# #
# Access control is a set of rules you can use to restrict user access to certain # Access control is a list of rules defining the authorizations applied for one
# resources. # resource to users or group of users.
# Any (apply to anyone), per-user or per-group rules can be defined.
# #
# If 'access_control' is not defined, ACL rules are disabled and the `allow` default # If 'access_control' is not defined, ACL rules are disabled and the `bypass`
# policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow # rule is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined. # the rules defined.
# #
# Note: One can use the wildcard * to match any subdomain. # Note: One can use the wildcard * to match any subdomain.
# It must stand at the beginning of the pattern. (example: *.mydomain.com) # It must stand at the beginning of the pattern. (example: *.mydomain.com)
# #
# Note: You must put the pattern in simple quotes when using the wildcard for the YAML # Note: You must put patterns containing wildcards between simple quotes for the YAML
# to be syntaxically correct. # to be syntaxically correct.
# #
# Definition: A `rule` is an object with the following keys: `domain`, `policy` # Definition: A `rule` is an object with the following keys: `domain`, `subject`,
# and `resources`. # `policy` and `resources`.
#
# - `domain` defines which domain or set of domains the rule applies to. # - `domain` defines which domain or set of domains the rule applies to.
# - `policy` is the policy to apply to resources. It must be either `allow` or `deny`. #
# - `subject` defines the subject to apply authorizations to. This parameter is
# optional and matching any user if not provided. If provided, the parameter
# represents either a user or a group. It should be of the form 'user:<username>'
# or 'group:<groupname>'.
#
# - `policy` is the policy to apply to resources. It must be either `bypass`,
# `one_factor`, `two_factor` or `deny`.
#
# - `resources` is a list of regular expressions that matches a set of resources to # - `resources` is a list of regular expressions that matches a set of resources to
# apply the policy to. #  apply the policy to. This parameter is optional and matches any resource if not
# # provided.
# Note: Rules follow an order of priority defined as follows:
# In each category (`any`, `groups`, `users`), the latest rules have the highest
# priority. In other words, it means that if a given resource matches two rules in the
# same category, the latest one overrides the first one.
# Each category has also its own priority. That is, `users` has the highest priority, then
# `groups` and `any` has the lowest priority. It means if two rules in different categories
# match a given resource, the one in the category with the highest priority overrides the
# other one.
# #
# Note: the order of the rules is important. The first policy matching
# (domain, resource, subject) applies.
access_control: access_control:
# Default policy can either be `allow` or `deny`. # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
# It is the policy applied to any resource if it has not been overriden # It is the policy applied to any resource if there is no policy to be applied
# in the `any`, `groups` or `users` category. # to the user.
default_policy: deny default_policy: deny
# The rules that apply to anyone. rules:
# The value is a list of rules. # Rules applied to everyone
any:
- domain: public.example.com - domain: public.example.com
policy: two_factor policy: two_factor
- domain: single_factor.example.com - domain: single_factor.example.com
policy: one_factor policy: one_factor
# Group-based rules. The key is a group name and the value # Rules applied to 'admin' group
# is a list of rules.
groups:
admin:
# All resources in all domains
- domain: '*.example.com'
policy: two_factor
# Except mx2.mail.example.com (it restricts the first rule)
- domain: 'mx2.mail.example.com' - domain: 'mx2.mail.example.com'
subject: 'group:admin'
policy: deny policy: deny
dev: - domain: '*.example.com'
- domain: dev.example.com subject: 'group:admin'
policy: two_factor policy: two_factor
# Rules applied to 'dev' group
- domain: dev.example.com
resources: resources:
- '^/groups/dev/.*$' - '^/groups/dev/.*$'
subject: 'group:dev'
# User-based rules. The key is a user name and the value
# is a list of rules.
users:
john:
- domain: dev.example.com
policy: two_factor policy: two_factor
# Rules applied to user 'john'
- domain: dev.example.com
resources: resources:
- '^/users/john/.*$' - '^/users/john/.*$'
harry: subject: 'user:john'
- domain: dev.example.com
policy: two_factor policy: two_factor
# Rules applied to user 'harry'
- domain: dev.example.com
resources: resources:
- '^/users/harry/.*$' - '^/users/harry/.*$'
bob: subject: 'user:harry'
policy: two_factor
# Rules applied to user 'bob'
- domain: '*.mail.example.com' - domain: '*.mail.example.com'
subject: 'user:bob'
policy: two_factor policy: two_factor
- domain: 'dev.example.com' - domain: 'dev.example.com'
policy: two_factor
resources: resources:
- '^/users/bob/.*$' - '^/users/bob/.*$'
subject: 'user:bob'
policy: two_factor
# Configuration of session cookies # Configuration of session cookies

View File

@ -102,7 +102,7 @@ export function get_start_validation(handler: IdentityValidable,
let identity: Identity.Identity; let identity: Identity.Identity;
return handler.preValidationInit(req) return handler.preValidationInit(req)
.then(function (id: Identity.Identity) { .then((id: Identity.Identity) => {
identity = id; identity = id;
const email = identity.email; const email = identity.email;
const userid = identity.userid; const userid = identity.userid;
@ -116,7 +116,7 @@ export function get_start_validation(handler: IdentityValidable,
return createAndSaveToken(userid, handler.challenge(), return createAndSaveToken(userid, handler.challenge(),
vars.userDataStore); vars.userDataStore);
}) })
.then(function (token: string) { .then((token) => {
const host = req.get("Host"); const host = req.get("Host");
const link_url = util.format("https://%s%s?identity_token=%s", host, const link_url = util.format("https://%s%s?identity_token=%s", host,
postValidationEndpoint, token); postValidationEndpoint, token);
@ -125,11 +125,11 @@ export function get_start_validation(handler: IdentityValidable,
return vars.notifier.notify(identity.email, handler.mailSubject(), return vars.notifier.notify(identity.email, handler.mailSubject(),
link_url); link_url);
}) })
.then(function () { .then(() => {
handler.preValidationResponse(req, res); handler.preValidationResponse(req, res);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })
.catch(Exceptions.IdentityError, function (err: Error) { .catch(Exceptions.IdentityError, (err: Error) => {
handler.preValidationResponse(req, res); handler.preValidationResponse(req, res);
return BluebirdPromise.resolve(); return BluebirdPromise.resolve();
}) })

View File

@ -25,9 +25,7 @@ describe("authorization/Authorizer", function () {
beforeEach(function () { beforeEach(function () {
configuration = { configuration = {
default_policy: "deny", default_policy: "deny",
any: [], rules: []
users: {},
groups: {}
}; };
authorizer = new Authorizer(configuration, winston); authorizer = new Authorizer(configuration, winston);
}); });
@ -42,9 +40,10 @@ describe("authorization/Authorizer", function () {
}); });
it("should control access when multiple domain matcher is provided", function () { it("should control access when multiple domain matcher is provided", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "*.mail.example.com", domain: "*.mail.example.com",
policy: "two_factor", policy: "two_factor",
subject: "user:user1",
resources: [".*"] resources: [".*"]
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY);
@ -54,9 +53,10 @@ describe("authorization/Authorizer", function () {
}); });
it("should allow access to all resources when resources is not provided", function () { it("should allow access to all resources when resources is not provided", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "*.mail.example.com", domain: "*.mail.example.com",
policy: "two_factor" policy: "two_factor",
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("mx1.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("mx1.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
@ -66,10 +66,11 @@ describe("authorization/Authorizer", function () {
describe("check user rules", function () { describe("check user rules", function () {
it("should allow access when user has a matching allowing rule", function () { it("should allow access when user has a matching allowing rule", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: [".*"] resources: [".*"],
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user1", ["group1"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user1", ["group1"]), Level.TWO_FACTOR);
@ -77,10 +78,11 @@ describe("authorization/Authorizer", function () {
}); });
it("should deny to other users", function () { it("should deny to other users", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: [".*"] resources: [".*"],
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user2", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/", "user2", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user2", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user2", ["group1"]), Level.DENY);
@ -88,10 +90,11 @@ describe("authorization/Authorizer", function () {
}); });
it("should allow user access only to specific resources", function () { it("should allow user access only to specific resources", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["/private/.*", "^/begin", "/end$"] resources: ["/private/.*", "^/begin", "/end$"],
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/private", "user1", ["group1"]), Level.DENY);
@ -106,18 +109,21 @@ describe("authorization/Authorizer", function () {
}); });
it("should allow access to multiple domains", function () { it("should allow access to multiple domains", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: [".*"] resources: [".*"],
subject: "user:user1"
}, { }, {
domain: "home1.example.com", domain: "home1.example.com",
policy: "one_factor", policy: "one_factor",
resources: [".*"] resources: [".*"],
subject: "user:user1"
}, { }, {
domain: "home2.example.com", domain: "home2.example.com",
policy: "deny", policy: "deny",
resources: [".*"] resources: [".*"],
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home1.example.com", "/", "user1", ["group1"]), Level.ONE_FACTOR); Assert.equal(authorizer.authorization("home1.example.com", "/", "user1", ["group1"]), Level.ONE_FACTOR);
@ -125,19 +131,22 @@ describe("authorization/Authorizer", function () {
Assert.equal(authorizer.authorization("home3.example.com", "/", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home3.example.com", "/", "user1", ["group1"]), Level.DENY);
}); });
it("should always apply latest rule", function () { it("should apply rules in order", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "one_factor",
resources: ["^/my/.*"] resources: ["/my/private/resource"],
subject: "user:user1"
}, { }, {
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "deny",
resources: ["^/my/private/.*"] resources: ["^/my/private/.*"],
subject: "user:user1"
}, { }, {
domain: "home.example.com", domain: "home.example.com",
policy: "one_factor", policy: "two_factor",
resources: ["/my/private/resource"] resources: ["^/my/.*"],
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/my/poney", "user1", ["group1"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/my/poney", "user1", ["group1"]), Level.TWO_FACTOR);
@ -148,19 +157,21 @@ describe("authorization/Authorizer", function () {
describe("check group rules", function () { describe("check group rules", function () {
it("should allow access when user is in group having a matching allowing rule", function () { it("should allow access when user is in group having a matching allowing rule", function () {
configuration.groups["group1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["^/$"] resources: ["^/$"],
}]; subject: "group:group1"
configuration.groups["group2"] = [{ }, {
domain: "home.example.com", domain: "home.example.com",
policy: "one_factor", policy: "one_factor",
resources: ["^/test$"] resources: ["^/test$"],
subject: "group:group2"
}, { }, {
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "deny",
resources: ["^/private$"] resources: ["^/private$"],
subject: "group:group2"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", Assert.equal(authorizer.authorization("home.example.com", "/", "user1",
["group1", "group2", "group3"]), Level.TWO_FACTOR); ["group1", "group2", "group3"]), Level.TWO_FACTOR);
@ -176,9 +187,9 @@ describe("authorization/Authorizer", function () {
describe("check any rules", function () { describe("check any rules", function () {
it("should control access when any rules are defined", function () { it("should control access when any rules are defined", function () {
configuration.any = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "bypass",
resources: ["^/public$"] resources: ["^/public$"]
}, { }, {
domain: "home.example.com", domain: "home.example.com",
@ -186,11 +197,11 @@ describe("authorization/Authorizer", function () {
resources: ["^/private$"] resources: ["^/private$"]
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/public", "user1", Assert.equal(authorizer.authorization("home.example.com", "/public", "user1",
["group1", "group2", "group3"]), Level.TWO_FACTOR); ["group1", "group2", "group3"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user1", Assert.equal(authorizer.authorization("home.example.com", "/private", "user1",
["group1", "group2", "group3"]), Level.DENY); ["group1", "group2", "group3"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/public", "user4", Assert.equal(authorizer.authorization("home.example.com", "/public", "user4",
["group5"]), Level.TWO_FACTOR); ["group5"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user4", Assert.equal(authorizer.authorization("home.example.com", "/private", "user4",
["group5"]), Level.DENY); ["group5"]), Level.DENY);
}); });
@ -208,10 +219,11 @@ describe("authorization/Authorizer", function () {
}); });
it("should deny access to one resource when defined", function () { it("should deny access to one resource when defined", function () {
configuration.users["user1"] = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "deny",
resources: ["/test"] resources: ["/test"],
subject: "user:user1"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.BYPASS); Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/test", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/test", "user1", ["group1"]), Level.DENY);
@ -229,39 +241,30 @@ describe("authorization/Authorizer", function () {
// admin is in groups ["admins"] // admin is in groups ["admins"]
// john is in groups ["dev", "admin-private"] // john is in groups ["dev", "admin-private"]
// harry is in groups ["dev"] // harry is in groups ["dev"]
configuration.any = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["^/public$", "^/$"] resources: ["^/public$", "^/$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.groups["admins"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: [".*"]
}];
configuration.groups["admin-private"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/?.*"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/john$"]
}];
configuration.users["harry"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/harry"]
}, { }, {
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "two_factor",
resources: ["^/dev/b.*$"] resources: [".*"],
subject: "group:admins"
}, {
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/?.*"],
subject: "group:admin-private"
}, {
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/john$"],
subject: "user:john"
}, {
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/harry"],
subject: "user:harry"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/", "admin", ["admins"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/", "admin", ["admins"]), Level.TWO_FACTOR);
@ -275,8 +278,8 @@ describe("authorization/Authorizer", function () {
Assert.equal(authorizer.authorization("home.example.com", "/", "john", ["dev", "admin-private"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/public", "john", ["dev", "admin-private"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/public", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "john", ["dev", "admin-private"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/dev", "john", ["dev", "admin-private"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/admin", "john", ["dev", "admin-private"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/admin", "john", ["dev", "admin-private"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "john", ["dev", "admin-private"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private/john", "john", ["dev", "admin-private"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/private/john", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
@ -284,7 +287,7 @@ describe("authorization/Authorizer", function () {
Assert.equal(authorizer.authorization("home.example.com", "/", "harry", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/", "harry", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/public", "harry", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/public", "harry", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "harry", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/dev", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "harry", ["dev"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/admin", "harry", ["dev"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/admin", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "harry", ["dev"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "harry", ["dev"]), Level.DENY);
@ -292,49 +295,50 @@ describe("authorization/Authorizer", function () {
Assert.equal(authorizer.authorization("home.example.com", "/private/harry", "harry", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/private/harry", "harry", ["dev"]), Level.TWO_FACTOR);
}); });
it("should control access when allowed at group level and denied at user level", function () { it("should allow when allowed at group level and denied at user level", function () {
configuration.groups["dev"] = [{ configuration.rules = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "deny",
resources: ["^/dev/bob$"] resources: ["^/dev/bob$"],
subject: "user:john"
}, {
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"],
subject: "group:dev"
}]; }];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY);
}); });
it("should control access when allowed at 'any' level and denied at user level", function () { it("should allow access when allowed at 'any' level and denied at user level", function () {
configuration.any = [{ configuration.rules = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"],
subject: "user:john"
}, {
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["^/dev/?.*$"] resources: ["^/dev/?.*$"]
}]; }];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY);
}); });
it("should control access when allowed at 'any' level and denied at group level", function () { it("should allow access when allowed at 'any' level and denied at group level", function () {
configuration.any = [{ configuration.rules = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"],
subject: "group:dev"
}, {
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["^/dev/?.*$"] resources: ["^/dev/?.*$"]
}]; }];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY);
@ -344,17 +348,17 @@ describe("authorization/Authorizer", function () {
// the priority from least to most is 'default_policy', 'all', 'group', 'user' // the priority from least to most is 'default_policy', 'all', 'group', 'user'
// and the first rules in each category as a lower priority than the latest. // and the first rules in each category as a lower priority than the latest.
// You can think of it that way: they override themselves inside each category. // You can think of it that way: they override themselves inside each category.
configuration.any = [{ configuration.rules = [{
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["^/dev/?.*$"] resources: ["^/dev/?.*$"],
}]; subject: "user:john"
configuration.groups["dev"] = [{ }, {
domain: "home.example.com", domain: "home.example.com",
policy: "deny", policy: "deny",
resources: ["^/dev/bob$"] resources: ["^/dev/bob$"],
}]; subject: "group:dev"
configuration.users["john"] = [{ }, {
domain: "home.example.com", domain: "home.example.com",
policy: "two_factor", policy: "two_factor",
resources: ["^/dev/?.*$"] resources: ["^/dev/?.*$"]

View File

@ -24,6 +24,24 @@ function MatchResource(actualResource: string) {
}; };
} }
function MatchSubject(user: string, groups: string[]) {
return (rule: ACLRule) => {
// If no subject, matches anybody
if (!rule.subject) return true;
if (rule.subject.startsWith("user:")) {
const ruleUser = rule.subject.split(":")[1];
if (user == ruleUser) return true;
}
if (rule.subject.startsWith("group:")) {
const ruleGroup = rule.subject.split(":")[1];
if (groups.indexOf(ruleGroup) > -1) return true;
}
return false;
};
}
export class Authorizer implements IAuthorizer { export class Authorizer implements IAuthorizer {
private logger: Winston; private logger: Winston;
private readonly configuration: ACLConfiguration; private readonly configuration: ACLConfiguration;
@ -33,39 +51,16 @@ export class Authorizer implements IAuthorizer {
this.configuration = configuration; this.configuration = configuration;
} }
private getMatchingUserRules(user: string, domain: string, resource: string): ACLRule[] { private getMatchingRules(domain: string, resource: string, user: string, groups: string[]): ACLRule[] {
const userRules = this.configuration.users[user]; const rules = this.configuration.rules;
if (!userRules) return [];
return userRules.filter(MatchDomain(domain)).filter(MatchResource(resource));
}
private getMatchingGroupRules(groups: string[], domain: string, resource: string): ACLRule[] {
const that = this;
// There is no ordering between group rules. That is, when a user belongs to 2 groups, there is no
// guarantee one set of rules has precedence on the other one.
const groupRules = groups.reduce(function (rules: ACLRule[], group: string) {
const groupRules = that.configuration.groups[group];
if (groupRules) rules = rules.concat(groupRules);
return rules;
}, []);
return groupRules.filter(MatchDomain(domain)).filter(MatchResource(resource));
}
private getMatchingAllRules(domain: string, resource: string): ACLRule[] {
const rules = this.configuration.any;
if (!rules) return []; if (!rules) return [];
return rules.filter(MatchDomain(domain)).filter(MatchResource(resource)); return rules
.filter(MatchDomain(domain))
.filter(MatchResource(resource))
.filter(MatchSubject(user, groups));
} }
authorization(domain: string, resource: string, user: string, groups: string[]): Level { private ruleToLevel(policy: string): Level {
if (!this.configuration) return Level.BYPASS;
const allRules = this.getMatchingAllRules(domain, resource);
const groupRules = this.getMatchingGroupRules(groups, domain, resource);
const userRules = this.getMatchingUserRules(user, domain, resource);
const rules = allRules.concat(groupRules).concat(userRules).reverse();
const policy = rules.map(r => r.policy).concat([this.configuration.default_policy])[0];
if (policy == "bypass") { if (policy == "bypass") {
return Level.BYPASS; return Level.BYPASS;
} else if (policy == "one_factor") { } else if (policy == "one_factor") {
@ -75,4 +70,14 @@ export class Authorizer implements IAuthorizer {
} }
return Level.DENY; return Level.DENY;
} }
authorization(domain: string, resource: string, user: string, groups: string[]): Level {
if (!this.configuration) return Level.BYPASS;
const rules = this.getMatchingRules(domain, resource, user, groups);
return (rules.length > 0)
? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule
: this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy
}
} }

View File

@ -125,32 +125,26 @@ describe("configuration/ConfigurationParser", function () {
const userConfig = buildYamlConfig(); const userConfig = buildYamlConfig();
userConfig.access_control = { userConfig.access_control = {
default_policy: "deny", default_policy: "deny",
any: [{ rules: [{
domain: "www.example.com",
policy: "two_factor",
subject: "user:user"
}, {
domain: "public.example.com", domain: "public.example.com",
policy: "two_factor" policy: "two_factor"
}],
users: {
"user": [{
domain: "www.example.com",
policy: "two_factor"
}] }]
},
groups: {}
}; };
const config = ConfigurationParser.parse(userConfig); const config = ConfigurationParser.parse(userConfig);
Assert.deepEqual(config.access_control, { Assert.deepEqual(config.access_control, {
default_policy: "deny", default_policy: "deny",
any: [{ rules: [{
domain: "www.example.com",
policy: "two_factor",
subject: "user:user"
}, {
domain: "public.example.com", domain: "public.example.com",
policy: "two_factor" policy: "two_factor"
}],
users: {
"user": [{
domain: "www.example.com",
policy: "two_factor"
}] }]
},
groups: {}
} as ACLConfiguration); } as ACLConfiguration);
}); });
@ -161,9 +155,7 @@ describe("configuration/ConfigurationParser", function () {
const config = ConfigurationParser.parse(userConfig); const config = ConfigurationParser.parse(userConfig);
Assert.deepEqual(config.access_control, { Assert.deepEqual(config.access_control, {
default_policy: "bypass", default_policy: "bypass",
any: [], rules: []
users: {},
groups: {}
}); });
}); });
}); });

View File

@ -11,9 +11,7 @@ describe("configuration/SessionConfigurationBuilder", function () {
const configuration: Configuration = { const configuration: Configuration = {
access_control: { access_control: {
default_policy: "deny", default_policy: "deny",
any: [], rules: []
users: {},
groups: {}
}, },
totp: { totp: {
issuer: "authelia.com" issuer: "authelia.com"

View File

@ -4,11 +4,31 @@ import Assert = require("assert");
describe("configuration/schema/AclConfiguration", function() { describe("configuration/schema/AclConfiguration", function() {
it("should complete ACLConfiguration", function() { it("should complete ACLConfiguration", function() {
const configuration: ACLConfiguration = {}; const configuration: ACLConfiguration = {};
const newConfiguration = complete(configuration); const [newConfiguration, errors] = complete(configuration);
Assert.deepEqual(newConfiguration.default_policy, "bypass"); Assert.deepEqual(newConfiguration.default_policy, "bypass");
Assert.deepEqual(newConfiguration.any, []); Assert.deepEqual(newConfiguration.rules, []);
Assert.deepEqual(newConfiguration.groups, {}); });
Assert.deepEqual(newConfiguration.users, {});
it("should return errors when subject is not good", function() {
const configuration: ACLConfiguration = {
default_policy: "deny",
rules: [{
domain: "dev.example.com",
subject: "user:abc",
policy: "bypass"
}, {
domain: "dev.example.com",
subject: "user:def",
policy: "bypass"
}, {
domain: "dev.example.com",
subject: "badkey:abc",
policy: "bypass"
}]
};
const [newConfiguration, errors] = complete(configuration);
Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]);
}); });
}); });

View File

@ -3,22 +3,17 @@ export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor";
export type ACLRule = { export type ACLRule = {
domain: string; domain: string;
policy: ACLPolicy;
resources?: string[]; resources?: string[];
subject?: string;
policy: ACLPolicy;
}; };
export type ACLDefaultRules = ACLRule[];
export type ACLGroupsRules = { [group: string]: ACLRule[]; };
export type ACLUsersRules = { [user: string]: ACLRule[]; };
export interface ACLConfiguration { export interface ACLConfiguration {
default_policy?: ACLPolicy; default_policy?: ACLPolicy;
any?: ACLDefaultRules; rules?: ACLRule[];
groups?: ACLGroupsRules;
users?: ACLUsersRules;
} }
export function complete(configuration: ACLConfiguration): ACLConfiguration { export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] {
const newConfiguration: ACLConfiguration = (configuration) const newConfiguration: ACLConfiguration = (configuration)
? JSON.parse(JSON.stringify(configuration)) : {}; ? JSON.parse(JSON.stringify(configuration)) : {};
@ -26,17 +21,21 @@ export function complete(configuration: ACLConfiguration): ACLConfiguration {
newConfiguration.default_policy = "bypass"; newConfiguration.default_policy = "bypass";
} }
if (!newConfiguration.any) { if (!newConfiguration.rules) {
newConfiguration.any = []; newConfiguration.rules = [];
} }
if (!newConfiguration.groups) { if (newConfiguration.rules.length > 0) {
newConfiguration.groups = {}; const errors: string[] = [];
newConfiguration.rules.forEach((r, idx) => {
if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) {
errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`);
}
});
if (errors.length > 0) {
return [newConfiguration, errors];
}
} }
if (!newConfiguration.users) { return [newConfiguration, []];
newConfiguration.users = {};
}
return newConfiguration;
} }

View File

@ -27,10 +27,14 @@ export function complete(
JSON.stringify(configuration)); JSON.stringify(configuration));
const errors: string[] = []; const errors: string[] = [];
newConfiguration.access_control = const [acls, aclsErrors] = AclConfigurationComplete(
AclConfigurationComplete(
newConfiguration.access_control); newConfiguration.access_control);
newConfiguration.access_control = acls;
if (aclsErrors.length > 0) {
errors.concat(aclsErrors);
}
const [backend, error] = const [backend, error] =
AuthenticationBackendComplete( AuthenticationBackendComplete(
newConfiguration.authentication_backend); newConfiguration.authentication_backend);

View File

@ -6,12 +6,12 @@ describe("configuration/schema/NotifierConfiguration", function() {
const configuration: NotifierConfiguration = {}; const configuration: NotifierConfiguration = {};
const [newConfiguration, error] = complete(configuration); const [newConfiguration, error] = complete(configuration);
Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}) Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"});
}); });
it("should ensure correct key is provided", function() { it("should ensure correct key is provided", function() {
const configuration = { const configuration = {
abc: 'badvalue' abc: "badvalue"
}; };
const [newConfiguration, error] = complete(configuration as any); const [newConfiguration, error] = complete(configuration as any);

View File

@ -246,7 +246,7 @@ describe("routes/verify/get", function () {
it("should fail when endpoint is protected by two factors", function () { it("should fail when endpoint is protected by two factors", function () {
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
mocks.config.access_control.default_policy = "one_factor"; mocks.config.access_control.default_policy = "one_factor";
mocks.config.access_control.any = [{ mocks.config.access_control.rules = [{
domain: "secret.example.com", domain: "secret.example.com",
policy: "two_factor" policy: "two_factor"
}]; }];

View File

@ -10,7 +10,7 @@ describe("storage/mongo/MongoCollection", function () {
let mongoClientStub: MongoClientStub; let mongoClientStub: MongoClientStub;
let findStub: Sinon.SinonStub; let findStub: Sinon.SinonStub;
let findOneStub: Sinon.SinonStub; let findOneStub: Sinon.SinonStub;
let insertStub: Sinon.SinonStub; let insertOneStub: Sinon.SinonStub;
let updateStub: Sinon.SinonStub; let updateStub: Sinon.SinonStub;
let removeStub: Sinon.SinonStub; let removeStub: Sinon.SinonStub;
let countStub: Sinon.SinonStub; let countStub: Sinon.SinonStub;
@ -21,7 +21,7 @@ describe("storage/mongo/MongoCollection", function () {
mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any);
findStub = mongoCollectionStub.find as Sinon.SinonStub; findStub = mongoCollectionStub.find as Sinon.SinonStub;
findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub;
insertStub = mongoCollectionStub.insert as Sinon.SinonStub; insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub;
updateStub = mongoCollectionStub.update as Sinon.SinonStub; updateStub = mongoCollectionStub.update as Sinon.SinonStub;
removeStub = mongoCollectionStub.remove as Sinon.SinonStub; removeStub = mongoCollectionStub.remove as Sinon.SinonStub;
countStub = mongoCollectionStub.count as Sinon.SinonStub; countStub = mongoCollectionStub.count as Sinon.SinonStub;
@ -63,11 +63,11 @@ describe("storage/mongo/MongoCollection", function () {
describe("insert", function () { describe("insert", function () {
it("should insert a document in the collection", function () { it("should insert a document in the collection", function () {
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
insertStub.returns(BluebirdPromise.resolve({})); insertOneStub.returns(BluebirdPromise.resolve({}));
return collection.insert({ key: "KEY" }) return collection.insert({ key: "KEY" })
.then(function () { .then(function () {
Assert(insertStub.calledWith({ key: "KEY" })); Assert(insertOneStub.calledWith({ key: "KEY" }));
}); });
}); });
}); });

View File

@ -40,7 +40,7 @@ export class MongoCollection implements ICollection {
insert(document: any): Bluebird<any> { insert(document: any): Bluebird<any> {
return this.collection() return this.collection()
.then((collection) => collection.insert(document)); .then((collection) => collection.insertOne(document));
} }
count(query: any): Bluebird<any> { count(query: any): Bluebird<any> {

View File

@ -33,7 +33,7 @@ Feature: User has access restricted access to domains
And I have access to "https://dev.example.com:8080/users/bob/secret.html" And I have access to "https://dev.example.com:8080/users/bob/secret.html"
And I have no access to "https://admin.example.com:8080/secret.html" And I have no access to "https://admin.example.com:8080/secret.html"
And I have access to "https://mx1.mail.example.com:8080/secret.html" And I have access to "https://mx1.mail.example.com:8080/secret.html"
And I have no access to "https://single_factor.example.com:8080/secret.html" And I have access to "https://single_factor.example.com:8080/secret.html"
And I have access to "https://mx2.mail.example.com:8080/secret.html" And I have access to "https://mx2.mail.example.com:8080/secret.html"
@need-registered-user-harry @need-registered-user-harry
@ -51,5 +51,5 @@ Feature: User has access restricted access to domains
And I have no access to "https://dev.example.com:8080/users/bob/secret.html" And I have no access to "https://dev.example.com:8080/users/bob/secret.html"
And I have no access to "https://admin.example.com:8080/secret.html" And I have no access to "https://admin.example.com:8080/secret.html"
And I have no access to "https://mx1.mail.example.com:8080/secret.html" And I have no access to "https://mx1.mail.example.com:8080/secret.html"
And I have no access to "https://single_factor.example.com:8080/secret.html" And I have access to "https://single_factor.example.com:8080/secret.html"
And I have no access to "https://mx2.mail.example.com:8080/secret.html" And I have no access to "https://mx2.mail.example.com:8080/secret.html"

View File

@ -14,24 +14,3 @@ Feature: Non authenticated users have no access to certain pages
| https://login.example.com:8080/api/u2f/sign | 401 | POST | | https://login.example.com:8080/api/u2f/sign | 401 | POST |
| https://login.example.com:8080/api/u2f/register_request | 401 | GET | | https://login.example.com:8080/api/u2f/register_request | 401 | GET |
| https://login.example.com:8080/api/u2f/register | 401 | POST | | https://login.example.com:8080/api/u2f/register | 401 | POST |
@needs-single_factor-config
@need-registered-user-john
Scenario: User does not have acces to second factor related endpoints when in single factor mode
Given I post "https://login.example.com:8080/api/firstfactor" with body:
| key | value |
| username | john |
| password | password |
Then I get the following status code when requesting:
| url | code | method |
| https://login.example.com:8080/secondfactor | 401 | GET |
| https://login.example.com:8080/secondfactor/u2f/identity/start | 401 | GET |
| https://login.example.com:8080/secondfactor/u2f/identity/finish | 401 | GET |
| https://login.example.com:8080/secondfactor/totp/identity/start | 401 | GET |
| https://login.example.com:8080/secondfactor/totp/identity/finish | 401 | GET |
| https://login.example.com:8080/api/totp | 401 | POST |
| https://login.example.com:8080/api/u2f/sign_request | 401 | GET |
| https://login.example.com:8080/api/u2f/sign | 401 | POST |
| https://login.example.com:8080/api/u2f/register_request | 401 | GET |
| https://login.example.com:8080/api/u2f/register | 401 | POST |

View File

@ -13,3 +13,4 @@ Feature: User can access certain subdomains with single factor
Scenario: User can login using basic authentication Scenario: User can login using basic authentication
When I request "https://single_factor.example.com:8080/secret.html" with username "john" and password "password" using basic authentication When I request "https://single_factor.example.com:8080/secret.html" with username "john" and password "password" using basic authentication
Then I receive the secret page Then I receive the secret page

View File

@ -1,16 +0,0 @@
@needs-single_factor-config
Feature: Server is configured as a single factor only server
@need-registered-user-john
Scenario: User is redirected to service after first factor if allowed
When I visit "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"
And I login with user "john" and password "password"
Then I'm redirected to "https://public.example.com:8080/secret.html"
@need-registered-user-john
Scenario: User is correctly redirected according to default redirection URL
When I visit "https://login.example.com:8080"
And I login with user "john" and password "password"
Then I'm redirected to "https://login.example.com:8080/loggedin"
And I sleep for 5 seconds
Then I'm redirected to "https://home.example.com:8080/"

View File

@ -33,7 +33,6 @@ function requestAndExpectStatusCode(ctx: any, url: string, method: string,
Assert.equal(statusCode, expectedStatusCode); Assert.equal(statusCode, expectedStatusCode);
} }
catch (e) { catch (e) {
console.log(url);
console.log("%s (actual) != %s (expected)", statusCode, console.log("%s (actual) != %s (expected)", statusCode,
expectedStatusCode); expectedStatusCode);
throw e; throw e;