Refine access control with per resource ACLs

ACLs can now be defined by subdomain AND resource using pattern matching
with regular expressions.
It allows a very fine-grained access control to backend resources.

[Note] For using example environmnent, user must update its /etc/hosts with
new subdomains updated in README.
This commit is contained in:
Clement Michaud 2017-09-03 15:22:09 +02:00
parent 72612e00aa
commit cf16272a73
37 changed files with 1043 additions and 495 deletions

View File

@ -18,9 +18,8 @@ addons:
- auth.test.local
- home.test.local
- public.test.local
- secret.test.local
- secret1.test.local
- secret2.test.local
- admin.test.local
- dev.test.local
- mx1.mail.test.local
- mx2.mail.test.local

View File

@ -48,7 +48,7 @@ without even configure anything.
Otherwise here are the available steps to deploy **Authelia** on your machine given
your configuration file is **/path/to/your/config.yml**. Note that you can create your
own the configuration file from **config.template.yml** at the root of the repo.
own the configuration file from [config.template.yml] at the root of the repo.
### With NPM
@ -91,11 +91,10 @@ Make sure you don't have anything listening on port 8080.
Add the following lines to your **/etc/hosts** to alias multiple subdomains so that nginx can redirect request to the correct virtual host.
127.0.0.1 public.test.local
127.0.0.1 secret.test.local
127.0.0.1 secret1.test.local
127.0.0.1 secret2.test.local
127.0.0.1 home.test.local
127.0.0.1 public.test.local
127.0.0.1 dev.test.local
127.0.0.1 admin.test.local
127.0.0.1 mx1.mail.test.local
127.0.0.1 mx2.mail.test.local
127.0.0.1 auth.test.local
@ -119,7 +118,7 @@ After few seconds the services should be running and you should be able to visit
When accessing the login page, a self-signed certificate exception should appear,
it has to be trusted before you can get to the target page. The certificate
must be trusted for each subdomain, therefore it is normal to see the exception
must also be trusted for each subdomain, therefore it is normal to see the exception
several times.
Below is what the login page looks like:
@ -128,37 +127,24 @@ Below is what the login page looks like:
## Features in details
### First factor with LDAP and ACL
An LDAP server has been deployed for you with the following credentials and
access control list:
- **john / password** is in the admin group and has access to the secret from
any subdomain.
- **bob / password** is in the dev group and has access to the secret from
- [secret.test.local](https://secret.test.local:8080/secret.html)
- [secret2.test.local](https://secret2.test.local:8080/secret.html)
- [home.test.local](https://home.test.local:8080/secret.html)
- [\*.mail.test.local](https://mx1.mail.test.local:8080/secret.html)
- **harry / password** is not in a group but has rules giving him has access to
the secret from
- [secret1.test.local](https://secret1.test.local:8080/secret.html)
- [home.test.local](https://home.test.local:8080/secret.html)
You can use them in the login page. If everything is ok, the second factor
page should appear as shown below. Otherwise you'll get an error message notifying
your credentials are wrong.
### First factor using an LDAP server
**Authelia** uses an LDAP server as the backend for storing credentials.
When authentication is needed, the user is redirected to the login page which
corresponds to the first factor. Authelia tries to bind the username and password
against the configured LDAP backend.
You can find an example of the configuration of the LDAP backend in [config.template.yml].
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/second_factor.png" width="400">
### Second factor with TOTP
In **Authelia**, you need to register a per user TOTP (Time-Based One Time Password) secret before
In **Authelia**, you can register a per user TOTP (Time-Based One Time Password) secret before
authenticating. To do that, you need to click on the register button. It will
send a link to the user email address. Since this is an example, no email will
be sent, the link is rather delivered in the file
send a link to the user email address defined in the LDAP.
Since this is an example, no email will be sent, the link is rather delivered in the file
**/tmp/notifications/notification.txt**. Paste the link in your browser and you'll get
your secret in QRCode and Base32 formats. You can use
your secret in QRCode and Base32 format. You can use
[Google Authenticator]
to store them and get the generated tokens with the app.
@ -169,8 +155,8 @@ to store them and get the generated tokens with the app.
USB security keys. U2F is one of the most secure authentication protocol and is
already available for Google, Facebook, Github accounts and more.
Like TOTP, U2F requires you register your security key before authenticating.
To do so, click on the register button. This will send a link to the
Like TOTP, U2F requires you register a security key before authenticating.
To do so, click on the register link. This will send a link to the
user email address. Since this is an example, no email will be sent, the
link is rather delivered in the file **/tmp/notifications/notification.txt**. Paste
the link in your browser and you'll be asking to touch the token of your device
@ -190,18 +176,18 @@ Paste the link in your browser and you should be able to reset the password.
<img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">
### Access Control
With **Authelia**, you can define your own access control rules for restricting
the user access to some subdomains. Those rules are defined in the
configuration file and can be set either for everyone, per-user or per-group policies.
Check out the *config.template.yml* to see how they are defined.
With **Authelia**, you can define your own access control rules for finely restricting
user access to some resources and subdomains. Those rules are defined and fully documented
in the configuration file. They can apply to users, groups or everyone.
Check out [config.template.yml] to see how they are defined.
### Session management with Redis
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in the [configuration file](#authelia-configuration).
When your users authenticate against Authelia, sessions are stored in a Redis key/value store. You can specify your own Redis instance in [config.template.yml].
## Documentation
### Authelia configuration
The configuration of the server is defined in the file
**configuration.template.yml**. All the details are documented there.
[config.template.yml]. All the details are documented there.
You can specify another configuration file by giving it as first argument of
**Authelia**.
@ -246,4 +232,5 @@ Follow [contributing](CONTRIBUTORS.md) file.
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
[auth_request]: http://nginx.org/en/docs/http/ngx_http_auth_request_module.html
[Google Authenticator]: https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
[config.template.yml]: https://github.com/clems4ever/authelia/blob/master/config.template.yml

View File

@ -49,38 +49,84 @@ ldap:
# Access Control
#
# Access control is a set of rules you can use to restrict the user access.
# Default (anyone), per-user or per-group rules can be defined.
# Access control is a set of rules you can use to restrict user access to certain
# resources.
# Any (apply to anyone), per-user or per-group rules can be defined.
#
# If 'access_control' is not defined, ACL rules are disabled and a default policy
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below.
# If no rule is provided, all domains are denied.
# If 'access_control' is not defined, ACL rules are disabled and the `allow` default
# policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined.
#
# Note: One can use the wildcard * to match any subdomain.
# 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
# to be syntaxically correct.
#
# Definition: A `rule` is an object with the following keys: `domain`, `policy`
# and `resources`.
# - `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`.
# - `resources` is a list of regular expressions that matches a set of resources to
# apply the policy to.
#
# 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.
#
# One can use the wildcard * to match any subdomain.
# Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
# Note 2: You must put the pattern in simple quotes when using the wildcard.
access_control:
# The default policy. Applies to any user
default:
- public.test.local
# Default policy can either be `allow` or `deny`.
# It is the policy applied to any resource if it has not been overriden
# in the `any`, `groups` or `users` category.
default_policy: deny
# Group based policies. The key is a group name and the value
# is the domain to allow access to.
# The rules that apply to anyone.
# The value is a list of rules.
any:
- domain: public.test.local
policy: allow
# Group-based rules. The key is a group name and the value
# is a list of rules.
groups:
admin:
- '*.test.local'
# All resources in all domains
- domain: '*.test.local'
policy: allow
# Except mx2.mail.test.local (it restricts the first rule)
- domain: 'mx2.mail.test.local'
policy: deny
dev:
- secret.test.local
- secret2.test.local
- domain: dev.test.local
policy: allow
resources:
- '^/groups/dev/.*$'
# Group based policies. The key is a group name and the value
# is the domain to allow access to.
# User-based rules. The key is a user name and the value
# is a list of rules.
users:
john:
- domain: dev.test.local
policy: allow
resources:
- '^/users/john/.*$'
harry:
- secret1.test.local
- domain: dev.test.local
policy: allow
resources:
- '^/users/harry/.*$'
bob:
- '*.mail.test.local'
- domain: '*.mail.test.local'
policy: allow
- domain: 'dev.test.local'
policy: allow
resources:
- '^/users/bob/.*$'
# Configuration of session cookies

View File

@ -61,27 +61,53 @@ ldap:
# Note 1: It must stand at the beginning of the pattern. (example: *.mydomain.com)
# Note 2: You must put the pattern in simple quotes when using the wildcard.
access_control:
# The default policy. Applies to any user
default:
- public.test.local
# Default policy can either be `allow` or `deny`.
# It is the policy applied to any resource if it has not been overriden
# in the `any`, `groups` or `users` category.
default_policy: deny
# Group based policies. The key is a group name and the value
# is the domain to allow access to.
# The rules that apply to anyone.
# The value is a list of rules.
any:
- domain: public.test.local
policy: allow
# Group-based rules. The key is a group name and the value
# is a list of rules.
groups:
admin:
- '*.test.local'
# All resources in all domains
- domain: '*.test.local'
policy: allow
# Except mx2.mail.test.local (it restricts the first rule)
- domain: 'mx2.mail.test.local'
policy: deny
dev:
- secret.test.local
- secret2.test.local
- domain: dev.test.local
policy: allow
resources:
- '^/groups/dev/.*$'
# Group based policies. The key is a group name and the value
# is the domain to allow access to.
# User-based rules. The key is a user name and the value
# is a list of rules.
users:
john:
- domain: dev.test.local
policy: allow
resources:
- '^/users/john/.*$'
harry:
- secret1.test.local
- domain: dev.test.local
policy: allow
resources:
- '^/users/harry/.*$'
bob:
- '*.mail.test.local'
- domain: '*.mail.test.local'
policy: allow
- domain: 'dev.test.local'
policy: allow
resources:
- '^/users/bob/.*$'
# Configuration of session cookies
#

View File

@ -14,10 +14,8 @@ services:
- example-network
# aliases:
# - home.test.local
# - secret.test.local
# - secret1.test.local
# - secret2.test.local
# - mx1.mail.test.local
# - mx2.mail.test.local
# - public.test.local
# - admin.test.local
# - dev.test.local
# - auth.test.local

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,132 @@
<!DOCTYPE>
<html>
<head>
<title>Home page</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
<h1>Access the secret</h1>
<span style="font-size: 1.2em; color: red">You need to log in to access the secret!</span><br/><br/>
Try to access it using one of the following links to test access control powered by Authelia.<br/>
<ul>
<li>
public.test.local <a href="https://public.test.local:8080/"> / index.html</a>
</li>
<li>
secret.test.local
<ul>
<li>Groups
<ul>
<li>
<a href="https://dev.test.local:8080/groups/admin/secret.html"> / groups / admins / secret.html</a>
</li>
<li>
<a href="https://dev.test.local:8080/groups/dev/secret.html"> / groups / dev / secret.html</a>
</li>
</ul>
</li>
<li>Users
<ul>
<li>
<a href="https://dev.test.local:8080/users/john/secret.html"> / users / john / secret.html</a>
</li>
<li>
<a href="https://dev.test.local:8080/users/harry/secret.html"> / users / harry / secret.html</a>
</li>
<li>
<a href="https://dev.test.local:8080/users/bob/secret.html"> / users / bob / secret.html</a>
</li>
</ul>
</li>
</ul>
</li>
<li>
admin.test.local <a href="https://admin.test.local:8080/secret.html"> / secret.html</a>
</li>
<li>
mx1.main.test.local <a href="https://mx1.mail.test.local:8080/secret.html"> / secret.html</a>
</li>
<li>
mx2.main.test.local <a href="https://mx2.mail.test.local:8080/secret.html"> / secret.html</a>
</li>
</ul>
You can also log off by visiting the following <a href="https://auth.test.local:8080/logout?redirect=https://home.test.local:8080/">link</a>.
<h1>List of users</h1>
Here is the list of credentials you can log in with to test access control.<br/>
<br/>
Once first factor is passed, you will need to follow the links to register a secret for the second factor.<br/>
Authelia will send you a fictituous email that will be in the file
<strong>/tmp/notifications/notification.txt</strong>.<br/>
It will provide you with the link to complete the registration allowing you to authenticate with 2-factor.
<ul>
<li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li>
<li><strong>bob / password</strong>: belongs to <em>dev</em> group only.</li>
<li><strong>harry / password</strong>: does not belong to any group.</li>
</ul>
<h1>Access control rules</h1>
<p></p>These rules are extracted from the configuration file
<a href="https://github.com/clems4ever/authelia/blob/master/config.template.yml">config.template.yml</a>.</p>
<pre id="rules" style="border: 1px grey solid; padding: 20px; display: inline-block;">
# Default policy can either be `allow` or `deny`.
# It is the policy applied to any resource if it has not been overriden
# in the `any`, `groups` or `users` category.
default_policy: deny
# The rules that apply to anyone.
# The value is a list of rules.
any:
- domain: public.test.local
policy: allow
# Group-based rules. The key is a group name and the value
# is a list of rules.
groups:
admin:
# All resources in all domains
- domain: '*.test.local'
policy: allow
# Except mx2.mail.test.local (it restricts the first rule)
- domain: 'mx2.mail.test.local'
policy: deny
dev:
- domain: dev.test.local
policy: allow
resources:
- '^/groups/dev/.*$'
# User-based rules. The key is a user name and the value
# is a list of rules.
users:
john:
- domain: dev.test.local
policy: allow
resources:
- '^/users/john/.*$'
harry:
- domain: dev.test.local
policy: allow
resources:
- '^/users/harry/.*$'
bob:
- domain: '*.mail.test.local'
policy: allow
- domain: 'dev.test.local'
policy: allow
resources:
- '^/users/bob/.*$'
- domain: 'dev.test.local'
policy: allow
resources:
- '^/users/harry/.*$'</pre>
</body>
</html>

View File

@ -1,82 +0,0 @@
<!DOCTYPE>
<html>
<head>
<title>Home page</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
<h1>Access the secret</h1>
You need to log in to access the secret!<br/><br/>
Try to access it via one of the following links.<br/>
<ul>
<li>
<a href="https://public.test.local:8080/secret.html">public.test.local</a>
</li>
<li>
<a href="https://secret.test.local:8080/secret.html">secret.test.local</a>
</li>
<li>
<a href="https://secret1.test.local:8080/secret.html">secret1.test.local</a>
</li>
<li>
<a href="https://secret2.test.local:8080/secret.html">secret2.test.local</a>
</li>
<li>
<a href="https://mx1.mail.test.local:8080/secret.html">mx1.mail.test.local</a>
</li>
<li>
<a href="https://mx2.mail.test.local:8080/secret.html">mx2.mail.test.local</a>
</li>
</ul>
You can also log off by visiting the following <a href="https://auth.test.local:8080/logout?redirect=https://home.test.local:8080/">link</a>.
<h1>List of users</h1>
Here is the list of credentials you can log in with to test access control.
<ul>
<li><strong>john / password</strong>: belongs to <em>admin</em> and <em>dev</em> groups.</li>
<li><strong>bob / password</strong>: belongs to <em>dev</em> group only.</li>
<li><strong>harry / password</strong>: does not belong to any group.</li>
</ul>
<h1>Access control rules</h1>
<ul>
<li><strong>Default policy</strong>
<ul>
<li>public.test.local</li>
</ul>
</li>
<li><strong>Groups policy</strong>
<ul>
<li>admin
<ul>
<li>*.test.local</li>
</ul>
</li>
<li>dev
<ul>
<li>secret.test.local</li>
<li>secret2.test.local</li>
</ul>
</li>
</ul>
</li>
<li><strong>Users policy</strong>
<ul>
<li>harry
<ul>
<li>secret1.test.local</li>
</ul>
</li>
<li>bob
<ul>
<li>*.mail.test.local</li>
</ul>
</li>
</ul>
</li>
</ul>
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -0,0 +1,13 @@
<!DOCTYPE>
<html>
<head>
<title>Public resource</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
<h1>Public resource</h1>
<p>This is a public resource.<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</p>
</body>
</html>

View File

@ -0,0 +1,10 @@
<html>
<head>
<title>Secret</title>
<link rel="icon" href="/icon.png" type="image/png" />
</head>
<body>
This is a very important secret!<br/>
Go back to <a href="https://home.test.local:8080/">home page</a>.
</body>
</html>

View File

@ -50,11 +50,20 @@ http {
server {
listen 443 ssl;
root /usr/share/nginx/html;
root /usr/share/nginx/html/home.test.local;
server_name secret1.test.local secret2.test.local secret.test.local
home.test.local mx1.mail.test.local mx2.mail.test.local
public.test.local;
server_name home.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
}
server {
listen 443 ssl;
root /usr/share/nginx/html/public.test.local;
server_name public.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
@ -69,7 +78,115 @@ http {
proxy_pass http://authelia/verify;
}
location = /secret.html {
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403;
}
}
server {
listen 443 ssl;
root /usr/share/nginx/html/admin.test.local;
server_name admin.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
location /auth_verify {
internal;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://authelia/verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403;
}
}
server {
listen 443 ssl;
root /usr/share/nginx/html/dev.test.local;
server_name dev.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
location /auth_verify {
internal;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://authelia/verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;
proxy_set_header Redirect $redirect;
auth_request_set $user $upstream_http_remote_user;
proxy_set_header X-Forwarded-User $user;
auth_request_set $groups $upstream_http_remote_groups;
proxy_set_header Remote-Groups $groups;
error_page 401 =302 https://auth.test.local:8080?redirect=$redirect;
error_page 403 = https://auth.test.local:8080/error/403;
}
}
server {
listen 443 ssl;
root /usr/share/nginx/html/mail.test.local;
server_name mx1.mail.test.local mx2.mail.test.local;
ssl on;
ssl_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.key;
location /auth_verify {
internal;
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $http_host;
proxy_pass http://authelia/verify;
}
location / {
auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect;

View File

@ -1,35 +1,109 @@
import { ACLConfiguration } from "../configuration/Configuration";
import PatternBuilder from "./PatternBuilder";
import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/Configuration";
import { IAccessController } from "./IAccessController";
import { Winston } from "../../../types/Dependencies";
import { DomainMatcher } from "./DomainMatcher";
export class AccessController {
enum AccessReturn {
NO_MATCHING_RULES,
MATCHING_RULES_AND_ACCESS,
MATCHING_RULES_AND_NO_ACCESS
}
function AllowedRule(rule: ACLRule) {
return rule.policy == "allow";
}
function MatchDomain(actualDomain: string) {
return function (rule: ACLRule): boolean {
return DomainMatcher.match(actualDomain, rule.domain);
};
}
function MatchResource(actualResource: string) {
return function (rule: ACLRule): boolean {
// If resources key is not provided, the rule applies to all resources.
if (!rule.resources) return true;
for (let i = 0; i < rule.resources.length; ++i) {
const regexp = new RegExp(rule.resources[i]);
if (regexp.test(actualResource)) return true;
}
return false;
};
}
function SelectPolicy(rule: ACLRule): ("allow" | "deny") {
return rule.policy;
}
export class AccessController implements IAccessController {
private logger: Winston;
private patternBuilder: PatternBuilder;
private readonly configuration: ACLConfiguration;
constructor(configuration: ACLConfiguration, logger_: Winston) {
this.logger = logger_;
this.patternBuilder = new PatternBuilder(configuration, logger_);
this.configuration = configuration;
}
isDomainAllowedForUser(domain: string, user: string, groups: string[]): boolean {
const allowed_domains = this.patternBuilder.getAllowedDomains(user, groups);
private isAccessAllowedInRules(rules: ACLRule[], domain: string, resource: string): AccessReturn {
if (!rules)
return AccessReturn.NO_MATCHING_RULES;
// Allow all matcher
if (allowed_domains.length == 1 && allowed_domains[0] == "*") return true;
const policies = rules.map(SelectPolicy);
this.logger.debug("ACL: trying to match %s with %s", domain,
JSON.stringify(allowed_domains));
for (let i = 0; i < allowed_domains.length; ++i) {
const allowed_domain = allowed_domains[i];
if (allowed_domain.startsWith("*") &&
domain.endsWith(allowed_domain.substr(1))) {
if (rules.length > 0) {
if (policies[0] == "allow") {
return AccessReturn.MATCHING_RULES_AND_ACCESS;
}
else {
return AccessReturn.MATCHING_RULES_AND_NO_ACCESS;
}
}
return AccessReturn.NO_MATCHING_RULES;
}
private getMatchingUserRules(user: string, domain: string, resource: string): ACLRule[] {
const userRules = this.configuration.users[user];
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 [];
return rules.filter(MatchDomain(domain)).filter(MatchResource(resource));
}
private isAccessAllowedDefaultPolicy(): boolean {
return this.configuration.default_policy == "allow";
}
isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean {
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 access = this.isAccessAllowedInRules(rules, domain, resource);
if (access == AccessReturn.MATCHING_RULES_AND_ACCESS)
return true;
}
else if (domain == allowed_domain) {
return true;
}
}
else if (access == AccessReturn.MATCHING_RULES_AND_NO_ACCESS)
return false;
return this.isAccessAllowedDefaultPolicy();
}
}

View File

@ -0,0 +1,12 @@
export class DomainMatcher {
static match(domain: string, allowedDomain: string): boolean {
if (allowedDomain.startsWith("*") &&
domain.endsWith(allowedDomain.substr(1))) {
return true;
}
else if (domain == allowedDomain) {
return true;
}
}
}

View File

@ -0,0 +1,4 @@
export interface IAccessController {
isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean;
}

View File

@ -1,61 +0,0 @@
import { Winston } from "../../../types/Dependencies";
import { ACLConfiguration, ACLGroupsRules, ACLUsersRules, ACLDefaultRules } from "../configuration/Configuration";
import objectPath = require("object-path");
export default class AccessControlPatternBuilder {
logger: Winston;
configuration: ACLConfiguration;
constructor(configuration: ACLConfiguration | undefined, logger_: Winston) {
this.configuration = configuration;
this.logger = logger_;
}
private buildFromGroups(groups: string[]): string[] {
let allowed_domains: string[] = [];
const groups_policy = objectPath.get<ACLConfiguration, ACLGroupsRules>(this.configuration, "groups");
if (groups_policy) {
for (let i = 0; i < groups.length; ++i) {
const group = groups[i];
if (group in groups_policy) {
const group_policy: string[] = groups_policy[group];
allowed_domains = allowed_domains.concat(groups_policy[group]);
}
}
}
return allowed_domains;
}
private buildFromUser(user: string): string[] {
let allowed_domains: string[] = [];
const users_policy = objectPath.get<ACLConfiguration, ACLUsersRules>(this.configuration, "users");
if (users_policy) {
if (user in users_policy) {
allowed_domains = allowed_domains.concat(users_policy[user]);
}
}
return allowed_domains;
}
getAllowedDomains(user: string, groups: string[]): string[] {
if (!this.configuration) {
this.logger.debug("No access control rules found." +
"Default policy to allow all.");
return ["*"]; // No configuration means, no restrictions.
}
let allowed_domains: string[] = [];
const default_policy = objectPath.get<ACLConfiguration, ACLDefaultRules>(this.configuration, "default");
if (default_policy) {
allowed_domains = allowed_domains.concat(default_policy);
}
allowed_domains = allowed_domains.concat(this.buildFromGroups(groups));
allowed_domains = allowed_domains.concat(this.buildFromUser(user));
this.logger.debug("ACL: user \'%s\' is allowed access to %s", user,
JSON.stringify(allowed_domains));
return allowed_domains;
}
}

View File

@ -35,12 +35,21 @@ type UserName = string;
type GroupName = string;
type DomainPattern = string;
export type ACLDefaultRules = DomainPattern[];
export type ACLGroupsRules = { [group: string]: string[]; };
export type ACLUsersRules = { [user: string]: string[]; };
export type ACLPolicy = 'deny' | 'allow';
export type ACLRule = {
domain: string;
policy: ACLPolicy;
resources?: string[];
}
export type ACLDefaultRules = ACLRule[];
export type ACLGroupsRules = { [group: string]: ACLRule[]; };
export type ACLUsersRules = { [user: string]: ACLRule[]; };
export interface ACLConfiguration {
default: ACLDefaultRules;
default_policy: ACLPolicy;
any: ACLDefaultRules;
groups: ACLGroupsRules;
users: ACLUsersRules;
}

View File

@ -6,7 +6,7 @@ import exceptions = require("../../Exceptions");
import winston = require("winston");
import AuthenticationValidator = require("../../AuthenticationValidator");
import ErrorReplies = require("../../ErrorReplies");
import {  ServerVariablesHandler } from "../../ServerVariablesHandler";
import { ServerVariablesHandler } from "../../ServerVariablesHandler";
import AuthenticationSession = require("../../AuthenticationSession");
function verify_filter(req: express.Request, res: express.Response): BluebirdPromise<void> {
@ -27,10 +27,11 @@ function verify_filter(req: express.Request, res: express.Response): BluebirdPro
const groups = authSession.groups;
const host = objectPath.get<express.Request, string>(req, "headers.host");
const domain = host.split(":")[0];
console.log(domain);
const path = objectPath.get<express.Request, string>(req, "headers.x-original-uri");
const isAllowed = accessController.isDomainAllowedForUser(domain, username, groups);
const domain = host.split(":")[0];
const isAllowed = accessController.isAccessAllowed(domain, path, username, groups);
if (!isAllowed) return BluebirdPromise.reject(
new exceptions.DomainAccessDenied("User '" + username + "' does not have access to " + domain));

View File

@ -9,10 +9,15 @@ Feature: User has access restricted access to domains
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://dev.test.local:8080/groups/admin/secret.html |
| https://dev.test.local:8080/groups/dev/secret.html |
| https://dev.test.local:8080/users/john/secret.html |
| https://dev.test.local:8080/users/harry/secret.html |
| https://dev.test.local:8080/users/bob/secret.html |
| https://admin.test.local:8080/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
And I have no access to:
| url |
| https://mx2.mail.test.local:8080/secret.html |
@need-registered-user-bob
@ -24,13 +29,16 @@ Feature: User has access restricted access to domains
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://dev.test.local:8080/groups/dev/secret.html |
| https://dev.test.local:8080/users/bob/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |
And I have no access to:
| url |
| https://secret1.test.local:8080/secret.html |
| https://dev.test.local:8080/groups/admin/secret.html |
| https://admin.test.local:8080/secret.html |
| https://dev.test.local:8080/users/john/secret.html |
| https://dev.test.local:8080/users/harry/secret.html |
@need-registered-user-harry
Scenario: User harry has restricted access
@ -41,10 +49,13 @@ Feature: User has access restricted access to domains
Then I have access to:
| url |
| https://public.test.local:8080/secret.html |
| https://secret1.test.local:8080/secret.html |
| https://dev.test.local:8080/users/harry/secret.html |
And I have no access to:
| url |
| https://secret.test.local:8080/secret.html |
| https://secret2.test.local:8080/secret.html |
| https://dev.test.local:8080/groups/dev/secret.html |
| https://dev.test.local:8080/users/bob/secret.html |
| https://dev.test.local:8080/groups/admin/secret.html |
| https://admin.test.local:8080/secret.html |
| https://dev.test.local:8080/users/john/secret.html |
| https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html |

View File

@ -18,19 +18,19 @@ Feature: User validate first factor
Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0"
When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsecret.test.local%3A8080%2Fsecret.html"
When I visit "https://admin.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password"
And I use "Sec0" as TOTP token handle
And I click on "TOTP"
Then I'm redirected to "https://secret.test.local:8080/secret.html"
Then I'm redirected to "https://admin.test.local:8080/secret.html"
Scenario: User fails TOTP second factor
When I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsecret.test.local%3A8080%2Fsecret.html"
When I visit "https://admin.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password"
And I use "BADTOKEN" as TOTP token
And I click on "TOTP"
Then I get a notification of type "error" with message "Problem with TOTP validation."
Scenario: Logout redirects user to redirect URL given in parameter
When I visit "https://auth.test.local:8080/logout?redirect=https://www.google.fr"
Then I'm redirected to "https://www.google.fr"
When I visit "https://auth.test.local:8080/logout?redirect=https://home.test.local:8080/"
Then I'm redirected to "https://home.test.local:8080/"

View File

@ -1,8 +1,7 @@
Feature: User is correctly redirected
Scenario: User is redirected to authelia when he is not authenticated
Given I'm on https://home.test.local:8080
When I click on the link to secret.test.local
When I visit "https://public.test.local:8080"
Then I'm redirected to "https://auth.test.local:8080/"
@need-registered-user-john
@ -15,9 +14,9 @@ Feature: User is correctly redirected
And I click on "TOTP"
Then I'm redirected to "https://public.test.local:8080/secret.html"
Scenario: User Harry does not have access to https://secret.test.local:8080/secret.html and thus he must get an error 403
Scenario: User Harry does not have access to admin domain and thus he must get an error 403
When I register TOTP and login with user "harry" and password "password"
And I visit "https://secret.test.local:8080/secret.html"
And I visit "https://admin.test.local:8080/secret.html"
Then I get an error 403

View File

@ -5,15 +5,15 @@ Feature: Authelia keeps user sessions despite the application restart
When the application restarts
Then I have access to:
| url |
| https://secret.test.local:8080/secret.html |
| https://admin.test.local:8080/secret.html |
@need-registered-user-john
Scenario: Secrets are stored even when Authelia restarts
When the application restarts
And I visit "https://secret.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fsecret.test.local%3A8080%2Fsecret.html"
And I visit "https://admin.test.local:8080/secret.html" and get redirected "https://auth.test.local:8080/?redirect=https%3A%2F%2Fadmin.test.local%3A8080%2Fsecret.html"
And I login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle
And I click on "TOTP"
Then I have access to:
| url |
| https://secret.test.local:8080/secret.html |
| https://admin.test.local:8080/secret.html |

View File

@ -81,7 +81,7 @@ and I use TOTP token handle {stringInDoubleQuotes}",
}
Then("I have access to:", function (dataTable: Cucumber.TableDefinition) {
const promises = [];
const promises: any = [];
for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url;
promises.push(hasAccessToSecret(url, this));

View File

@ -47,6 +47,9 @@ function CustomWorld() {
.findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click();
})
.then(function () {
return that.driver.sleep(500);
});
};

View File

@ -11,7 +11,8 @@ describe("test session configuration builder", function () {
it("should return session options without redis options", function () {
const configuration: AppConfiguration = {
access_control: {
default: [],
default_policy: "deny",
any: [],
users: {},
groups: {}
},
@ -81,7 +82,8 @@ describe("test session configuration builder", function () {
it("should return session options with redis options", function () {
const configuration: AppConfiguration = {
access_control: {
default: [],
default_policy: "deny",
any: [],
users: {},
groups: {}
},

View File

@ -1,8 +1,8 @@
import assert = require("assert");
import Assert = require("assert");
import winston = require("winston");
import { AccessController } from "../../../../src/server/lib/access_control/AccessController";
import { ACLConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import { AccessController } from "../../../../src/server/lib/access_control/AccessController";
import { ACLConfiguration, ACLRule } from "../../../../src/server/lib/configuration/Configuration";
describe("test access control manager", function () {
let accessController: AccessController;
@ -10,44 +10,344 @@ describe("test access control manager", function () {
beforeEach(function () {
configuration = {
default: [],
default_policy: "deny",
any: [],
users: {},
groups: {}
};
accessController = new AccessController(configuration, winston);
});
describe("check access control matching", function () {
describe("check access control with default policy to deny", function () {
beforeEach(function () {
configuration.default = ["home.example.com", "*.public.example.com"];
configuration.users = {
user1: ["user1.example.com", "user1.mail.example.com"]
};
configuration.groups = {
group1: ["secret2.example.com"],
group2: ["secret.example.com", "secret1.example.com"]
};
configuration.default_policy = "deny";
});
it("should allow access to secret.example.com", function () {
assert(accessController.isDomainAllowedForUser("secret.example.com", "user", ["group1", "group2"]));
it("should deny access when no rule is provided", function () {
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
});
it("should deny access to secret3.example.com", function () {
assert(!accessController.isDomainAllowedForUser("secret3.example.com", "user", ["group1", "group2"]));
it("should control access when multiple domain matcher is provided", function () {
configuration.users["user1"] = [{
domain: "*.mail.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.server.mail.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("mail.example.com", "/", "user1", ["group1"]));
});
it("should allow access to home.example.com", function () {
assert(accessController.isDomainAllowedForUser("home.example.com", "user", ["group1", "group2"]));
it("should allow access to all resources when resources is not provided", function () {
configuration.users["user1"] = [{
domain: "*.mail.example.com",
policy: "allow"
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.mail.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("mx1.server.mail.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("mail.example.com", "/", "user1", ["group1"]));
});
it("should allow access to user1.example.com", function () {
assert(accessController.isDomainAllowedForUser("user1.example.com", "user1", ["group1", "group2"]));
describe("check user rules", function () {
it("should allow access when user has a matching allowing rule", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/another/resource", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user1", ["group1"]));
});
it("should allow access *.public.example.com", function () {
assert(accessController.isDomainAllowedForUser("user.public.example.com", "nouser", []));
assert(accessController.isDomainAllowedForUser("test.public.example.com", "nouser", []));
it("should deny to other users", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user2", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/another/resource", "user2", ["group1"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user2", ["group1"]));
});
it("should allow user access only to specific resources", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["/private/.*", "^/begin", "/end$"]
}];
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/class", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/middle/private/class", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/begin", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/not/begin", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/abc/end", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/abc/end/x", "user1", ["group1"]));
});
it("should allow access to multiple domains", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}, {
domain: "home1.example.com",
policy: "allow",
resources: [".*"]
}, {
domain: "home2.example.com",
policy: "deny",
resources: [".*"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home1.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home2.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home3.example.com", "/", "user1", ["group1"]));
});
it("should always apply latest rule", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/my/.*"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/my/private/.*"]
}, {
domain: "home.example.com",
policy: "allow",
resources: ["/my/private/resource"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/my/poney", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/my/private/duck", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/my/private/resource", "user1", ["group1"]));
});
});
describe("check group rules", function () {
it("should allow access when user is in group having a matching allowing rule", function () {
configuration.groups["group1"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/$"]
}];
configuration.groups["group2"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/test$"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/private$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1",
["group1", "group2", "group3"]));
Assert(accessController.isAccessAllowed("home.example.com", "/test", "user1",
["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1",
["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("another.home.example.com", "/", "user1",
["group1", "group2", "group3"]));
});
});
});
describe("check all rules", function () {
it("should control access when all rules are defined", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/public$"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/private$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/public", "user1",
["group1", "group2", "group3"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user1",
["group1", "group2", "group3"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "user4",
["group5"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private", "user4",
["group5"]));
});
});
describe("check access control with default policy to allow", function () {
beforeEach(function () {
configuration.default_policy = "allow";
});
it("should allow access to anything when no rule is provided", function () {
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/test", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "user1", ["group1"]));
});
it("should deny access to one resource when defined", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["/test"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/test", "user1", ["group1"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "user1", ["group1"]));
});
});
describe("check access control with complete use case", function () {
beforeEach(function () {
configuration.default_policy = "deny";
});
it("should control access of multiple user (real use case)", function () {
// Let say we have three users: admin, john, harry.
// admin is in groups ["admins"]
// john is in groups ["dev", "admin-private"]
// harry is in groups ["dev"]
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/public$", "^/$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["admins"] = [{
domain: "home.example.com",
policy: "allow",
resources: [".*"]
}];
configuration.groups["admin-private"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/?.*"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/john$"]
}];
configuration.users["harry"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/private/harry"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/b.*$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/admin", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/josh", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/john", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "admin", ["admins"]));
Assert(accessController.isAccessAllowed("home.example.com", "/", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/admin", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/josh", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/john", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "john", ["dev", "admin-private"]));
Assert(accessController.isAccessAllowed("home.example.com", "/", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/public", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/admin", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private/josh", "harry", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/private/john", "harry", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/private/harry", "harry", ["dev"]));
});
it("should control access when allowed at group level and denied at user level", function () {
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should control access when allowed at all level and denied at user level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should control access when allowed at all level and denied at group level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(!accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
it("should respect rules precedence", function () {
// 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.
// You can think of it that way: they override themselves inside each category.
configuration.any = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "allow",
resources: ["^/dev/?.*$"]
}];
Assert(accessController.isAccessAllowed("home.example.com", "/dev/john", "john", ["dev"]));
Assert(accessController.isAccessAllowed("home.example.com", "/dev/bob", "john", ["dev"]));
});
});
});

View File

@ -1,120 +0,0 @@
import assert = require("assert");
import winston = require("winston");
import PatternBuilder from "../../../../src/server/lib/access_control/PatternBuilder";
import { ACLConfiguration } from "../../../../src/server/lib/configuration/Configuration";
describe("test access control manager", function () {
describe("test access control pattern builder when no configuration is provided", () => {
it("should allow access to the user", () => {
const patternBuilder = new PatternBuilder(undefined, winston);
const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1"]);
assert.deepEqual(allowed_domains, ["*"]);
});
});
describe("test access control pattern builder", function () {
let patternBuilder: PatternBuilder;
let configuration: ACLConfiguration;
beforeEach(() => {
configuration = {
default: [],
users: {},
groups: {}
};
patternBuilder = new PatternBuilder(configuration, winston);
});
it("should deny all if nothing is defined in the config", function () {
const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains, []);
});
it("should allow domain test.example.com to all users if defined in" +
" default policy", function () {
configuration.default = ["test.example.com"];
const allowed_domains = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains, ["test.example.com"]);
});
it("should allow domain test.example.com to all users in group mygroup", function () {
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group1"]);
assert.deepEqual(allowed_domains0, []);
configuration.groups = {
mygroup: ["test.example.com"]
};
const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains1, []);
const allowed_domains2 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]);
assert.deepEqual(allowed_domains2, ["test.example.com"]);
});
it("should allow domain test.example.com based on per user config", function () {
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1"]);
assert.deepEqual(allowed_domains0, []);
configuration.users = {
user1: ["test.example.com"]
};
const allowed_domains1 = patternBuilder.getAllowedDomains("user", ["group1", "mygroup"]);
assert.deepEqual(allowed_domains1, []);
const allowed_domains2 = patternBuilder.getAllowedDomains("user1", ["group1", "mygroup"]);
assert.deepEqual(allowed_domains2, ["test.example.com"]);
});
it("should allow domains from user and groups", function () {
configuration.groups = {
group2: ["secret.example.com", "secret1.example.com"]
};
configuration.users = {
user: ["test.example.com"]
};
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains0, [
"secret.example.com",
"secret1.example.com",
"test.example.com",
]);
});
it("should allow domains from several groups", function () {
configuration.groups = {
group1: ["secret2.example.com"],
group2: ["secret.example.com", "secret1.example.com"]
};
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains0, [
"secret2.example.com",
"secret.example.com",
"secret1.example.com",
]);
});
it("should allow domains from several groups and default policy", function () {
configuration.default = ["home.example.com"];
configuration.groups = {
group1: ["secret2.example.com"],
group2: ["secret.example.com", "secret1.example.com"]
};
const allowed_domains0 = patternBuilder.getAllowedDomains("user", ["group1", "group2"]);
assert.deepEqual(allowed_domains0, [
"home.example.com",
"secret2.example.com",
"secret.example.com",
"secret1.example.com",
]);
});
});
});

View File

@ -1,5 +1,8 @@
import * as Assert from "assert";
import { UserConfiguration, LdapConfiguration } from "../../../../src/server/lib/configuration/Configuration";
import {
UserConfiguration,
LdapConfiguration, ACLConfiguration
} from "../../../../src/server/lib/configuration/Configuration";
import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter";
describe("test config adapter", function () {
@ -94,15 +97,17 @@ describe("test config adapter", function () {
it("should get the access_control config", function () {
const yaml_config = build_yaml_config();
yaml_config.access_control = {
default: [],
default_policy: "deny",
any: [],
users: {},
groups: {}
};
const config = ConfigurationAdapter.adapt(yaml_config);
Assert.deepEqual(config.access_control, {
default: [],
default_policy: "deny",
any: [],
users: {},
groups: {}
});
} as ACLConfiguration);
});
});

View File

@ -1,12 +0,0 @@
import sinon = require("sinon");
export interface AccessControllerMock {
isDomainAllowedForUser: sinon.SinonStub;
}
export function AccessControllerMock() {
return {
isDomainAllowedForUser: sinon.stub()
};
}

View File

@ -0,0 +1,15 @@
import Sinon = require("sinon");
import { IAccessController } from "../../../../src/server/lib/access_control/IAccessController";
export class AccessControllerStub implements IAccessController {
isAccessAllowedMock: Sinon.SinonStub;
constructor() {
this.isAccessAllowedMock = Sinon.stub();
}
isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean {
return this.isAccessAllowedMock(domain, resource, user, groups);
}
}

View File

@ -10,7 +10,7 @@ import AuthenticationSession = require("../../../../../src/server/lib/Authentica
import Endpoints = require("../../../../../src/server/endpoints");
import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator");
import AccessControllerMock = require("../../mocks/AccessController");
import { AccessControllerStub } from "../../mocks/AccessControllerStub";
import ExpressMock = require("../../mocks/express");
import ServerVariablesMock = require("../../mocks/ServerVariablesMock");
import { ServerVariables } from "../../../../../src/server/lib/ServerVariablesHandler";
@ -22,7 +22,7 @@ describe("test the first factor validation route", function () {
let groups: string[];
let configuration;
let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock;
let accessController: AccessControllerMock.AccessControllerMock;
let accessController: AccessControllerStub;
let serverVariables: ServerVariables;
beforeEach(function () {
@ -36,8 +36,8 @@ describe("test the first factor validation route", function () {
emails = ["test_ok@example.com"];
groups = ["group1", "group2" ];
accessController = AccessControllerMock.AccessControllerMock();
accessController.isDomainAllowedForUser.returns(true);
accessController = new AccessControllerStub();
accessController.isAccessAllowedMock.returns(true);
regulator = AuthenticationRegulatorMock.AuthenticationRegulatorMock();
regulator.regulate.returns(BluebirdPromise.resolve());

View File

@ -10,17 +10,17 @@ import BluebirdPromise = require("bluebird");
import express = require("express");
import ExpressMock = require("../../mocks/express");
import AccessControllerMock = require("../../mocks/AccessController");
import { AccessControllerStub } from "../../mocks/AccessControllerStub";
import ServerVariablesMock = require("../../mocks/ServerVariablesMock");
describe("test authentication token verification", function () {
let req: ExpressMock.RequestMock;
let res: ExpressMock.ResponseMock;
let accessController: AccessControllerMock.AccessControllerMock;
let accessController: AccessControllerStub;
beforeEach(function () {
accessController = AccessControllerMock.AccessControllerMock();
accessController.isDomainAllowedForUser.returns(true);
accessController = new AccessControllerStub();
accessController.isAccessAllowedMock.returns(true);
req = ExpressMock.RequestMock();
res = ExpressMock.ResponseMock();
@ -128,8 +128,8 @@ describe("test authentication token verification", function () {
req.headers.host = "test.example.com";
accessController.isDomainAllowedForUser.returns(false);
accessController.isDomainAllowedForUser.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true);
accessController.isAccessAllowedMock.returns(false);
accessController.isAccessAllowedMock.withArgs("test.example.com", "user", ["group1", "group2"]).returns(true);
return test_unauthorized_403({
first_factor: true,