Merge pull request #95 from clems4ever/acl-by-resources

Refine access control with per resource ACLs
This commit is contained in:
Clément Michaud 2017-09-24 21:54:18 +02:00 committed by GitHub
commit 7a2b45a66f
37 changed files with 1043 additions and 495 deletions

View File

@ -18,9 +18,8 @@ addons:
- auth.test.local - auth.test.local
- home.test.local - home.test.local
- public.test.local - public.test.local
- secret.test.local - admin.test.local
- secret1.test.local - dev.test.local
- secret2.test.local
- mx1.mail.test.local - mx1.mail.test.local
- mx2.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 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 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 ### 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. 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 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 mx1.mail.test.local
127.0.0.1 mx2.mail.test.local 127.0.0.1 mx2.mail.test.local
127.0.0.1 auth.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, 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 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. several times.
Below is what the login page looks like: Below is what the login page looks like:
@ -128,37 +127,24 @@ Below is what the login page looks like:
## Features in details ## Features in details
### First factor with LDAP and ACL ### First factor using an LDAP server
An LDAP server has been deployed for you with the following credentials and **Authelia** uses an LDAP server as the backend for storing credentials.
access control list: 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
- **john / password** is in the admin group and has access to the secret from against the configured LDAP backend.
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.
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"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/second_factor.png" width="400">
### Second factor with TOTP ### 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 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 send a link to the user email address defined in the LDAP.
be sent, the link is rather delivered in the file 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 **/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] [Google Authenticator]
to store them and get the generated tokens with the app. 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 USB security keys. U2F is one of the most secure authentication protocol and is
already available for Google, Facebook, Github accounts and more. already available for Google, Facebook, Github accounts and more.
Like TOTP, U2F requires you register your security key before authenticating. Like TOTP, U2F requires you register a security key before authenticating.
To do so, click on the register button. This will send a link to the 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 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 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 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"> <img src="https://raw.githubusercontent.com/clems4ever/authelia/master/images/reset_password.png" width="400">
### Access Control ### Access Control
With **Authelia**, you can define your own access control rules for restricting With **Authelia**, you can define your own access control rules for finely restricting
the user access to some subdomains. Those rules are defined in the user access to some resources and subdomains. Those rules are defined and fully documented
configuration file and can be set either for everyone, per-user or per-group policies. in the configuration file. They can apply to users, groups or everyone.
Check out the *config.template.yml* to see how they are defined. Check out [config.template.yml] to see how they are defined.
### Session management with Redis ### 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 ## Documentation
### Authelia configuration ### Authelia configuration
The configuration of the server is defined in the file 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 You can specify another configuration file by giving it as first argument of
**Authelia**. **Authelia**.
@ -246,4 +232,5 @@ Follow [contributing](CONTRIBUTORS.md) file.
[Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/ [Yubikey]: https://www.yubico.com/products/yubikey-hardware/yubikey4/
[auth_request]: http://nginx.org/en/docs/http/ngx_http_auth_request_module.html [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 [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
# #
# Access control is a set of rules you can use to restrict the user access. # Access control is a set of rules you can use to restrict user access to certain
# Default (anyone), per-user or per-group rules can be defined. # 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 # If 'access_control' is not defined, ACL rules are disabled and the `allow` default
# is applied, i.e., access is allowed to anyone. Otherwise restrictions follow # policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow
# the rules defined below. # the rules defined.
# If no rule is provided, all domains are denied. #
# 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: access_control:
# The default policy. Applies to any user # Default policy can either be `allow` or `deny`.
default: # It is the policy applied to any resource if it has not been overriden
- public.test.local # in the `any`, `groups` or `users` category.
default_policy: deny
# Group based policies. The key is a group name and the value # The rules that apply to anyone.
# is the domain to allow access to. # 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: groups:
admin: 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: dev:
- secret.test.local - domain: dev.test.local
- secret2.test.local policy: allow
resources:
- '^/groups/dev/.*$'
# Group based policies. The key is a group name and the value # User-based rules. The key is a user name and the value
# is the domain to allow access to. # is a list of rules.
users: users:
john:
- domain: dev.test.local
policy: allow
resources:
- '^/users/john/.*$'
harry: harry:
- secret1.test.local - domain: dev.test.local
policy: allow
resources:
- '^/users/harry/.*$'
bob: bob:
- '*.mail.test.local' - domain: '*.mail.test.local'
policy: allow
- domain: 'dev.test.local'
policy: allow
resources:
- '^/users/bob/.*$'
# Configuration of session cookies # 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 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. # Note 2: You must put the pattern in simple quotes when using the wildcard.
access_control: access_control:
# The default policy. Applies to any user # Default policy can either be `allow` or `deny`.
default: # It is the policy applied to any resource if it has not been overriden
- public.test.local # in the `any`, `groups` or `users` category.
default_policy: deny
# Group based policies. The key is a group name and the value # The rules that apply to anyone.
# is the domain to allow access to. # 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: groups:
admin: 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: dev:
- secret.test.local - domain: dev.test.local
- secret2.test.local policy: allow
resources:
- '^/groups/dev/.*$'
# Group based policies. The key is a group name and the value # User-based rules. The key is a user name and the value
# is the domain to allow access to. # is a list of rules.
users: users:
john:
- domain: dev.test.local
policy: allow
resources:
- '^/users/john/.*$'
harry: harry:
- secret1.test.local - domain: dev.test.local
policy: allow
resources:
- '^/users/harry/.*$'
bob: bob:
- '*.mail.test.local' - domain: '*.mail.test.local'
policy: allow
- domain: 'dev.test.local'
policy: allow
resources:
- '^/users/bob/.*$'
# Configuration of session cookies # Configuration of session cookies
# #

View File

@ -14,10 +14,8 @@ services:
- example-network - example-network
# aliases: # aliases:
# - home.test.local # - home.test.local
# - secret.test.local # - public.test.local
# - secret1.test.local # - admin.test.local
# - secret2.test.local # - dev.test.local
# - mx1.mail.test.local
# - mx2.mail.test.local
# - auth.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,128 @@ http {
server { server {
listen 443 ssl; 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 server_name home.test.local;
home.test.local mx1.mail.test.local mx2.mail.test.local
public.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;
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/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 on;
ssl_certificate /etc/ssl/server.crt; ssl_certificate /etc/ssl/server.crt;
@ -70,7 +187,7 @@ http {
proxy_pass http://authelia/verify; proxy_pass http://authelia/verify;
} }
location = /secret.html { location / {
auth_request /auth_verify; auth_request /auth_verify;
auth_request_set $redirect $upstream_http_redirect; auth_request_set $redirect $upstream_http_redirect;

View File

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

View File

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

View File

@ -7,13 +7,18 @@ Feature: User has access restricted access to domains
And I use "REGISTERED" as TOTP token handle And I use "REGISTERED" as TOTP token handle
And I click on "TOTP" And I click on "TOTP"
Then I have access to: Then I have access to:
| url | | url |
| https://public.test.local:8080/secret.html | | https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html | | https://dev.test.local:8080/groups/admin/secret.html |
| https://secret1.test.local:8080/secret.html | | https://dev.test.local:8080/groups/dev/secret.html |
| https://secret2.test.local:8080/secret.html | | https://dev.test.local:8080/users/john/secret.html |
| https://mx1.mail.test.local:8080/secret.html | | https://dev.test.local:8080/users/harry/secret.html |
| https://mx2.mail.test.local:8080/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 @need-registered-user-bob
Scenario: User bob has restricted access Scenario: User bob has restricted access
@ -22,15 +27,18 @@ Feature: User has access restricted access to domains
And I use "REGISTERED" as TOTP token handle And I use "REGISTERED" as TOTP token handle
And I click on "TOTP" And I click on "TOTP"
Then I have access to: Then I have access to:
| url | | url |
| https://public.test.local:8080/secret.html | | https://public.test.local:8080/secret.html |
| https://secret.test.local:8080/secret.html | | https://dev.test.local:8080/groups/dev/secret.html |
| https://secret2.test.local:8080/secret.html | | https://dev.test.local:8080/users/bob/secret.html |
| https://mx1.mail.test.local:8080/secret.html | | https://mx1.mail.test.local:8080/secret.html |
| https://mx2.mail.test.local:8080/secret.html | | https://mx2.mail.test.local:8080/secret.html |
And I have no access to: And I have no access to:
| url | | 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 @need-registered-user-harry
Scenario: User harry has restricted access Scenario: User harry has restricted access
@ -39,12 +47,15 @@ Feature: User has access restricted access to domains
And I use "REGISTERED" as TOTP token handle And I use "REGISTERED" as TOTP token handle
And I click on "TOTP" And I click on "TOTP"
Then I have access to: Then I have access to:
| url | | url |
| https://public.test.local:8080/secret.html | | 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: And I have no access to:
| url | | url |
| https://secret.test.local:8080/secret.html | | https://dev.test.local:8080/groups/dev/secret.html |
| https://secret2.test.local:8080/secret.html | | https://dev.test.local:8080/users/bob/secret.html |
| https://mx1.mail.test.local:8080/secret.html | | https://dev.test.local:8080/groups/admin/secret.html |
| https://mx2.mail.test.local:8080/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/" Given I visit "https://auth.test.local:8080/"
And I login with user "john" and password "password" And I login with user "john" and password "password"
And I register a TOTP secret called "Sec0" 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 login with user "john" and password "password"
And I use "Sec0" as TOTP token handle And I use "Sec0" as TOTP token handle
And I click on "TOTP" 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 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 login with user "john" and password "password"
And I use "BADTOKEN" as TOTP token And I use "BADTOKEN" as TOTP token
And I click on "TOTP" And I click on "TOTP"
Then I get a notification of type "error" with message "Problem with TOTP validation." Then I get a notification of type "error" with message "Problem with TOTP validation."
Scenario: Logout redirects user to redirect URL given in parameter Scenario: Logout redirects user to redirect URL given in parameter
When I visit "https://auth.test.local:8080/logout?redirect=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://www.google.fr" Then I'm redirected to "https://home.test.local:8080/"

View File

@ -1,8 +1,7 @@
Feature: User is correctly redirected Feature: User is correctly redirected
Scenario: User is redirected to authelia when he is not authenticated Scenario: User is redirected to authelia when he is not authenticated
Given I'm on https://home.test.local:8080 When I visit "https://public.test.local:8080"
When I click on the link to secret.test.local
Then I'm redirected to "https://auth.test.local:8080/" Then I'm redirected to "https://auth.test.local:8080/"
@need-registered-user-john @need-registered-user-john
@ -15,9 +14,9 @@ Feature: User is correctly redirected
And I click on "TOTP" And I click on "TOTP"
Then I'm redirected to "https://public.test.local:8080/secret.html" 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" 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 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 When the application restarts
Then I have access to: Then I have access to:
| url | | url |
| https://secret.test.local:8080/secret.html | | https://admin.test.local:8080/secret.html |
@need-registered-user-john @need-registered-user-john
Scenario: Secrets are stored even when Authelia restarts Scenario: Secrets are stored even when Authelia restarts
When the application 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 login with user "john" and password "password"
And I use "REGISTERED" as TOTP token handle And I use "REGISTERED" as TOTP token handle
And I click on "TOTP" And I click on "TOTP"
Then I have access to: Then I have access to:
| url | | 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) { Then("I have access to:", function (dataTable: Cucumber.TableDefinition) {
const promises = []; const promises: any = [];
for (let i = 0; i < dataTable.rows().length; i++) { for (let i = 0; i < dataTable.rows().length; i++) {
const url = (dataTable.hashes() as any)[i].url; const url = (dataTable.hashes() as any)[i].url;
promises.push(hasAccessToSecret(url, this)); promises.push(hasAccessToSecret(url, this));

View File

@ -47,6 +47,9 @@ function CustomWorld() {
.findElement(seleniumWebdriver.By.tagName("button")) .findElement(seleniumWebdriver.By.tagName("button"))
.findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]")) .findElement(seleniumWebdriver.By.xpath("//button[contains(.,'" + buttonText + "')]"))
.click(); .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 () { it("should return session options without redis options", function () {
const configuration: AppConfiguration = { const configuration: AppConfiguration = {
access_control: { access_control: {
default: [], default_policy: "deny",
any: [],
users: {}, users: {},
groups: {} groups: {}
}, },
@ -81,7 +82,8 @@ describe("test session configuration builder", function () {
it("should return session options with redis options", function () { it("should return session options with redis options", function () {
const configuration: AppConfiguration = { const configuration: AppConfiguration = {
access_control: { access_control: {
default: [], default_policy: "deny",
any: [],
users: {}, users: {},
groups: {} groups: {}
}, },

View File

@ -1,53 +1,353 @@
import assert = require("assert"); import Assert = require("assert");
import winston = require("winston"); import winston = require("winston");
import { AccessController } from "../../../../src/server/lib/access_control/AccessController"; import { AccessController } from "../../../../src/server/lib/access_control/AccessController";
import { ACLConfiguration } from "../../../../src/server/lib/configuration/Configuration"; import { ACLConfiguration, ACLRule } from "../../../../src/server/lib/configuration/Configuration";
describe("test access control manager", function () { describe("test access control manager", function () {
let accessController: AccessController; let accessController: AccessController;
let configuration: ACLConfiguration; let configuration: ACLConfiguration;
beforeEach(function () {
configuration = {
default_policy: "deny",
any: [],
users: {},
groups: {}
};
accessController = new AccessController(configuration, winston);
});
describe("check access control with default policy to deny", function () {
beforeEach(function () { beforeEach(function () {
configuration = { configuration.default_policy = "deny";
default: [],
users: {},
groups: {}
};
accessController = new AccessController(configuration, winston);
}); });
describe("check access control matching", function () { it("should deny access when no rule is provided", function () {
beforeEach(function () { Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
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"]
};
});
it("should allow access to secret.example.com", function () {
assert(accessController.isDomainAllowedForUser("secret.example.com", "user", ["group1", "group2"]));
});
it("should deny access to secret3.example.com", function () {
assert(!accessController.isDomainAllowedForUser("secret3.example.com", "user", ["group1", "group2"]));
});
it("should allow access to home.example.com", function () {
assert(accessController.isDomainAllowedForUser("home.example.com", "user", ["group1", "group2"]));
});
it("should allow access to user1.example.com", function () {
assert(accessController.isDomainAllowedForUser("user1.example.com", "user1", ["group1", "group2"]));
});
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 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 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"]));
});
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 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 * 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"; import { ConfigurationAdapter } from "../../../../src/server/lib/configuration/ConfigurationAdapter";
describe("test config adapter", function () { describe("test config adapter", function () {
@ -94,15 +97,17 @@ describe("test config adapter", function () {
it("should get the access_control config", function () { it("should get the access_control config", function () {
const yaml_config = build_yaml_config(); const yaml_config = build_yaml_config();
yaml_config.access_control = { yaml_config.access_control = {
default: [], default_policy: "deny",
any: [],
users: {}, users: {},
groups: {} groups: {}
}; };
const config = ConfigurationAdapter.adapt(yaml_config); const config = ConfigurationAdapter.adapt(yaml_config);
Assert.deepEqual(config.access_control, { Assert.deepEqual(config.access_control, {
default: [], default_policy: "deny",
any: [],
users: {}, users: {},
groups: {} 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 Endpoints = require("../../../../../src/server/endpoints");
import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator"); import AuthenticationRegulatorMock = require("../../mocks/AuthenticationRegulator");
import AccessControllerMock = require("../../mocks/AccessController"); import { AccessControllerStub } from "../../mocks/AccessControllerStub";
import ExpressMock = require("../../mocks/express"); import ExpressMock = require("../../mocks/express");
import ServerVariablesMock = require("../../mocks/ServerVariablesMock"); import ServerVariablesMock = require("../../mocks/ServerVariablesMock");
import { ServerVariables } from "../../../../../src/server/lib/ServerVariablesHandler"; import { ServerVariables } from "../../../../../src/server/lib/ServerVariablesHandler";
@ -22,7 +22,7 @@ describe("test the first factor validation route", function () {
let groups: string[]; let groups: string[];
let configuration; let configuration;
let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock; let regulator: AuthenticationRegulatorMock.AuthenticationRegulatorMock;
let accessController: AccessControllerMock.AccessControllerMock; let accessController: AccessControllerStub;
let serverVariables: ServerVariables; let serverVariables: ServerVariables;
beforeEach(function () { beforeEach(function () {
@ -36,8 +36,8 @@ describe("test the first factor validation route", function () {
emails = ["test_ok@example.com"]; emails = ["test_ok@example.com"];
groups = ["group1", "group2" ]; groups = ["group1", "group2" ];
accessController = AccessControllerMock.AccessControllerMock(); accessController = new AccessControllerStub();
accessController.isDomainAllowedForUser.returns(true); accessController.isAccessAllowedMock.returns(true);
regulator = AuthenticationRegulatorMock.AuthenticationRegulatorMock(); regulator = AuthenticationRegulatorMock.AuthenticationRegulatorMock();
regulator.regulate.returns(BluebirdPromise.resolve()); regulator.regulate.returns(BluebirdPromise.resolve());

View File

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