mirror of
https://github.com/0rangebananaspy/authelia.git
synced 2024-09-14 22:47:21 +07:00
Merge pull request #289 from clems4ever/remove-auth-methods
Introduce "bypass", "one_factor", "two_factor" and "deny" ACL rules
This commit is contained in:
commit
6c4d06b2a8
|
@ -22,50 +22,44 @@ storage:
|
|||
totp:
|
||||
issuer: example.com
|
||||
|
||||
# Authentication methods
|
||||
#
|
||||
# Authentication methods can be defined per subdomain.
|
||||
# There are currently two available methods: "single_factor" and "two_factor"
|
||||
authentication_methods:
|
||||
default_method: two_factor
|
||||
per_subdomain_methods:
|
||||
single_factor.example.com: single_factor
|
||||
|
||||
# Access Control
|
||||
#
|
||||
# Access control is a set of rules you can use to restrict user access to certain
|
||||
# resources.
|
||||
access_control:
|
||||
# Default policy can either be `allow` or `deny`.
|
||||
# Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`.
|
||||
default_policy: deny
|
||||
groups:
|
||||
admins:
|
||||
# All resources in all domains
|
||||
- domain: '*.example.com'
|
||||
policy: allow
|
||||
# Except mx2.mail.example.com (it restricts the first rule)
|
||||
#- domain: 'mx2.mail.example.com'
|
||||
# policy: deny
|
||||
|
||||
# User-based rules.
|
||||
users:
|
||||
john:
|
||||
- domain: dev.example.com
|
||||
policy: allow
|
||||
resources:
|
||||
- '^/users/john/.*$'
|
||||
harry:
|
||||
- domain: dev.example.com
|
||||
policy: allow
|
||||
resources:
|
||||
- '^/users/harry/.*$'
|
||||
bob:
|
||||
- domain: '*.mail.example.com'
|
||||
policy: allow
|
||||
- domain: 'dev.example.com'
|
||||
policy: allow
|
||||
resources:
|
||||
- '^/users/bob/.*$'
|
||||
rules:
|
||||
- domain: single_factor.example.com
|
||||
policy: one_factor
|
||||
|
||||
- domain: '*.example.com'
|
||||
subject: "group:admins"
|
||||
policy: two_factor
|
||||
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- '^/users/john/.*$'
|
||||
subject: "user:john"
|
||||
policy: two_factor
|
||||
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- '^/users/harry/.*$'
|
||||
subject: "user:harry"
|
||||
policy: two_factor
|
||||
|
||||
- domain: '*.mail.example.com'
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- '^/users/bob/.*$'
|
||||
subject: "user:bob"
|
||||
policy: two_factor
|
||||
|
||||
|
||||
# Configuration of the authentication regulation mechanism.
|
||||
regulation:
|
||||
|
|
|
@ -86,110 +86,96 @@ authentication_backend:
|
|||
## path: ./users_database.yml
|
||||
|
||||
|
||||
# Authentication methods
|
||||
#
|
||||
# Authentication methods can be defined per subdomain.
|
||||
# There are currently two available methods: "single_factor" and "two_factor"
|
||||
#
|
||||
# Note: by default a domain uses "two_factor" method.
|
||||
#
|
||||
# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and
|
||||
# values must be one of the two possible methods.
|
||||
#
|
||||
# Note: 'per_subdomain_methods' is optional.
|
||||
#
|
||||
# Note: authentication_methods is optional. If it is not set all sub-domains
|
||||
# are protected by two factors.
|
||||
authentication_methods:
|
||||
default_method: two_factor
|
||||
per_subdomain_methods:
|
||||
single_factor.example.com: single_factor
|
||||
|
||||
|
||||
# Access Control
|
||||
#
|
||||
# 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.
|
||||
# Access control is a list of rules defining the authorizations applied for one
|
||||
# resource to users or group of users.
|
||||
#
|
||||
# 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
|
||||
# If 'access_control' is not defined, ACL rules are disabled and the `bypass`
|
||||
# rule 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.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# Note: You must put patterns containing wildcards between simple quotes for the YAML
|
||||
# to be syntaxically correct.
|
||||
#
|
||||
# Definition: A `rule` is an object with the following keys: `domain`, `policy`
|
||||
# and `resources`.
|
||||
# Definition: A `rule` is an object with the following keys: `domain`, `subject`,
|
||||
# `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.
|
||||
# - `subject` defines the subject to apply authorizations to. This parameter is
|
||||
# optional and matching any user if not provided. If provided, the parameter
|
||||
# represents either a user or a group. It should be of the form 'user:<username>'
|
||||
# or 'group:<groupname>'.
|
||||
#
|
||||
# - `policy` is the policy to apply to resources. It must be either `bypass`,
|
||||
# `one_factor`, `two_factor` or `deny`.
|
||||
#
|
||||
# - `resources` is a list of regular expressions that matches a set of resources to
|
||||
# apply the policy to. This parameter is optional and matches any resource if not
|
||||
# provided.
|
||||
#
|
||||
# Note: the order of the rules is important. The first policy matching
|
||||
# (domain, resource, subject) applies.
|
||||
access_control:
|
||||
# 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 can either be `bypass`, `one_factor`, `two_factor` or `deny`.
|
||||
# It is the policy applied to any resource if there is no policy to be applied
|
||||
# to the user.
|
||||
default_policy: deny
|
||||
|
||||
# The rules that apply to anyone.
|
||||
# The value is a list of rules.
|
||||
any:
|
||||
rules:
|
||||
# Rules applied to everyone
|
||||
- domain: public.example.com
|
||||
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: '*.example.com'
|
||||
policy: allow
|
||||
# Except mx2.mail.example.com (it restricts the first rule)
|
||||
- domain: 'mx2.mail.example.com'
|
||||
policy: deny
|
||||
dev:
|
||||
- domain: dev.example.com
|
||||
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.example.com
|
||||
policy: allow
|
||||
resources:
|
||||
- '^/users/john/.*$'
|
||||
harry:
|
||||
- domain: dev.example.com
|
||||
policy: allow
|
||||
resources:
|
||||
- '^/users/harry/.*$'
|
||||
bob:
|
||||
- domain: '*.mail.example.com'
|
||||
policy: allow
|
||||
- domain: 'dev.example.com'
|
||||
policy: allow
|
||||
resources:
|
||||
- '^/users/bob/.*$'
|
||||
policy: two_factor
|
||||
- domain: single_factor.example.com
|
||||
policy: one_factor
|
||||
|
||||
# Rules applied to 'admin' group
|
||||
- domain: 'mx2.mail.example.com'
|
||||
subject: 'group:admin'
|
||||
policy: deny
|
||||
- domain: '*.example.com'
|
||||
subject: 'group:admin'
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to 'dev' group
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- '^/groups/dev/.*$'
|
||||
subject: 'group:dev'
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to user 'john'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- '^/users/john/.*$'
|
||||
subject: 'user:john'
|
||||
policy: two_factor
|
||||
|
||||
|
||||
# Rules applied to user 'harry'
|
||||
- domain: dev.example.com
|
||||
resources:
|
||||
- '^/users/harry/.*$'
|
||||
subject: 'user:harry'
|
||||
policy: two_factor
|
||||
|
||||
# Rules applied to user 'bob'
|
||||
- domain: '*.mail.example.com'
|
||||
subject: 'user:bob'
|
||||
policy: two_factor
|
||||
- domain: 'dev.example.com'
|
||||
resources:
|
||||
- '^/users/bob/.*$'
|
||||
subject: 'user:bob'
|
||||
policy: two_factor
|
||||
|
||||
|
||||
# Configuration of session cookies
|
||||
#
|
||||
#
|
||||
# The session cookies identify the user once logged in.
|
||||
session:
|
||||
# The name of the session cookie. (default: authelia_session).
|
||||
|
@ -197,7 +183,7 @@ session:
|
|||
|
||||
# The secret to encrypt the session cookie.
|
||||
secret: unsecure_session_secret
|
||||
|
||||
|
||||
# The time in ms before the cookie expires and session is reset.
|
||||
expiration: 3600000 # 1 hour
|
||||
|
||||
|
@ -206,9 +192,9 @@ session:
|
|||
|
||||
# The domain to protect.
|
||||
# Note: the authenticator must also be in that domain. If empty, the cookie
|
||||
# is restricted to the subdomain of the issuer.
|
||||
# is restricted to the subdomain of the issuer.
|
||||
domain: example.com
|
||||
|
||||
|
||||
# The redis connection details
|
||||
redis:
|
||||
host: redis
|
||||
|
@ -221,12 +207,12 @@ session:
|
|||
# It bans the user if too many attempts are done in a short period of
|
||||
# time.
|
||||
regulation:
|
||||
# The number of failed login attempts before user is banned.
|
||||
# The number of failed login attempts before user is banned.
|
||||
# Set it to 0 to disable regulation.
|
||||
max_retries: 3
|
||||
|
||||
# The time range during which the user can attempt login before being banned.
|
||||
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
|
||||
# The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window.
|
||||
find_time: 120
|
||||
|
||||
# The length of time before a banned user can login again.
|
||||
|
@ -239,7 +225,7 @@ storage:
|
|||
# The directory where the DB files will be saved
|
||||
## local:
|
||||
## path: /var/lib/authelia/store
|
||||
|
||||
|
||||
# Settings to connect to mongo server
|
||||
mongo:
|
||||
url: mongodb://mongo
|
||||
|
@ -259,13 +245,13 @@ notifier:
|
|||
## filename: /tmp/authelia/notification.txt
|
||||
|
||||
# Use your email account to send the notifications. You can use an app password.
|
||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
||||
# List of valid services can be found here: https://nodemailer.com/smtp/well-known/
|
||||
## email:
|
||||
## username: user@example.com
|
||||
## password: yourpassword
|
||||
## sender: admin@example.com
|
||||
## service: gmail
|
||||
|
||||
|
||||
# Use a SMTP server for sending notifications
|
||||
smtp:
|
||||
username: test
|
||||
|
|
|
@ -10,9 +10,7 @@ services:
|
|||
- ./dist/server/src/public_html:/usr/src/server/src/public_html
|
||||
- ./client:/usr/src/client
|
||||
- ./shared:/usr/src/shared
|
||||
- ./config.minimal.yml:/etc/authelia/config.yml:ro
|
||||
- /tmp/authelia:/tmp/authelia
|
||||
- ./users_database.yml:/etc/authelia/users_database.yml
|
||||
- ./config.template.yml:/etc/authelia/config.yml:ro
|
||||
environment:
|
||||
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
depends_on:
|
||||
|
|
27
docker-compose.minimal.dev.yml
Normal file
27
docker-compose.minimal.dev.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
version: '2'
|
||||
services:
|
||||
authelia:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.dev
|
||||
restart: always
|
||||
volumes:
|
||||
- ./server:/usr/src/server
|
||||
- ./dist/server/src/public_html:/usr/src/server/src/public_html
|
||||
- ./client:/usr/src/client
|
||||
- ./shared:/usr/src/shared
|
||||
- ./config.minimal.yml:/etc/authelia/config.yml:ro
|
||||
- /tmp/authelia:/tmp/authelia
|
||||
- ./users_database.yml:/etc/authelia/users_database.yml
|
||||
environment:
|
||||
- NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
depends_on:
|
||||
- redis
|
||||
networks:
|
||||
- example-network
|
||||
command:
|
||||
- "./node_modules/.bin/ts-node"
|
||||
- "-P"
|
||||
- "server/tsconfig.json"
|
||||
- "server/src/index.ts"
|
||||
- "/etc/authelia/config.yml"
|
|
@ -5,11 +5,11 @@ import U2f = require("u2f");
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import { AuthenticationSession } from "../../types/AuthenticationSession";
|
||||
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||
import { Level } from "./authentication/Level";
|
||||
|
||||
const INITIAL_AUTHENTICATION_SESSION: AuthenticationSession = {
|
||||
keep_me_logged_in: false,
|
||||
first_factor: false,
|
||||
second_factor: false,
|
||||
authentication_level: Level.NOT_AUTHENTICATED,
|
||||
last_activity_datetime: undefined,
|
||||
userid: undefined,
|
||||
email: undefined,
|
||||
|
|
|
@ -55,11 +55,19 @@ export class InvalidTOTPError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class DomainAccessDenied extends Error {
|
||||
export class NotAuthenticatedError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "DomainAccessDenied";
|
||||
(<any>Object).setPrototypeOf(this, DomainAccessDenied.prototype);
|
||||
this.name = "NotAuthenticatedError";
|
||||
(<any>Object).setPrototypeOf(this, NotAuthenticatedError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
export class NotAuthorizedError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = "NotAuthanticatedError";
|
||||
(<any>Object).setPrototypeOf(this, NotAuthorizedError.prototype);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,12 +5,13 @@ import objectPath = require("object-path");
|
|||
import Exceptions = require("./Exceptions");
|
||||
import { AuthenticationSessionHandler } from "./AuthenticationSessionHandler";
|
||||
import { IRequestLogger } from "./logging/IRequestLogger";
|
||||
import { Level } from "./authentication/Level";
|
||||
|
||||
export function validate(req: express.Request, logger: IRequestLogger): BluebirdPromise<void> {
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
const authSession = AuthenticationSessionHandler.get(req, logger);
|
||||
|
||||
if (!authSession.userid || !authSession.first_factor)
|
||||
if (!authSession.userid || authSession.authentication_level < Level.ONE_FACTOR)
|
||||
return reject(new Exceptions.FirstFactorValidationError(
|
||||
"First factor has not been validated yet."));
|
||||
|
||||
|
|
|
@ -102,7 +102,7 @@ export function get_start_validation(handler: IdentityValidable,
|
|||
let identity: Identity.Identity;
|
||||
|
||||
return handler.preValidationInit(req)
|
||||
.then(function (id: Identity.Identity) {
|
||||
.then((id: Identity.Identity) => {
|
||||
identity = id;
|
||||
const email = identity.email;
|
||||
const userid = identity.userid;
|
||||
|
@ -116,7 +116,7 @@ export function get_start_validation(handler: IdentityValidable,
|
|||
return createAndSaveToken(userid, handler.challenge(),
|
||||
vars.userDataStore);
|
||||
})
|
||||
.then(function (token: string) {
|
||||
.then((token) => {
|
||||
const host = req.get("Host");
|
||||
const link_url = util.format("https://%s%s?identity_token=%s", host,
|
||||
postValidationEndpoint, token);
|
||||
|
@ -125,11 +125,11 @@ export function get_start_validation(handler: IdentityValidable,
|
|||
return vars.notifier.notify(identity.email, handler.mailSubject(),
|
||||
link_url);
|
||||
})
|
||||
.then(function () {
|
||||
.then(() => {
|
||||
handler.preValidationResponse(req, res);
|
||||
return BluebirdPromise.resolve();
|
||||
})
|
||||
.catch(Exceptions.IdentityError, function (err: Error) {
|
||||
.catch(Exceptions.IdentityError, (err: Error) => {
|
||||
handler.preValidationResponse(req, res);
|
||||
return BluebirdPromise.resolve();
|
||||
})
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import ObjectPath = require("object-path");
|
||||
|
||||
import { AccessController } from "./access_control/AccessController";
|
||||
import { Configuration } from "./configuration/schema/Configuration";
|
||||
import { GlobalDependencies } from "../../types/Dependencies";
|
||||
import { UserDataStore } from "./storage/UserDataStore";
|
||||
|
|
|
@ -5,7 +5,7 @@ import { IUserDataStore } from "./storage/IUserDataStore";
|
|||
import { INotifier } from "./notifiers/INotifier";
|
||||
import { IRegulator } from "./regulation/IRegulator";
|
||||
import { Configuration } from "./configuration/schema/Configuration";
|
||||
import { IAccessController } from "./access_control/IAccessController";
|
||||
import { IAuthorizer } from "./authorization/IAuthorizer";
|
||||
import { IUsersDatabase } from "./authentication/backends/IUsersDatabase";
|
||||
|
||||
export interface ServerVariables {
|
||||
|
@ -17,5 +17,5 @@ export interface ServerVariables {
|
|||
notifier: INotifier;
|
||||
regulator: IRegulator;
|
||||
config: Configuration;
|
||||
accessController: IAccessController;
|
||||
authorizer: IAuthorizer;
|
||||
}
|
|
@ -20,8 +20,6 @@ import { INotifier } from "./notifiers/INotifier";
|
|||
import { Regulator } from "./regulation/Regulator";
|
||||
import { IRegulator } from "./regulation/IRegulator";
|
||||
import Configuration = require("./configuration/schema/Configuration");
|
||||
import { AccessController } from "./access_control/AccessController";
|
||||
import { IAccessController } from "./access_control/IAccessController";
|
||||
import { CollectionFactoryFactory } from "./storage/CollectionFactoryFactory";
|
||||
import { ICollectionFactory } from "./storage/ICollectionFactory";
|
||||
import { MongoCollectionFactory } from "./storage/mongo/MongoCollectionFactory";
|
||||
|
@ -29,12 +27,12 @@ import { IMongoClient } from "./connectors/mongo/IMongoClient";
|
|||
|
||||
import { GlobalDependencies } from "../../types/Dependencies";
|
||||
import { ServerVariables } from "./ServerVariables";
|
||||
import { MethodCalculator } from "./authentication/MethodCalculator";
|
||||
import { MongoClient } from "./connectors/mongo/MongoClient";
|
||||
import { IGlobalLogger } from "./logging/IGlobalLogger";
|
||||
import { SessionFactory } from "./authentication/backends/ldap/SessionFactory";
|
||||
import { IUsersDatabase } from "./authentication/backends/IUsersDatabase";
|
||||
import { FileUsersDatabase } from "./authentication/backends/file/FileUsersDatabase";
|
||||
import { Authorizer } from "./authorization/Authorizer";
|
||||
|
||||
class UserDataStoreFactory {
|
||||
static create(config: Configuration.Configuration, globalLogger: IGlobalLogger): BluebirdPromise<UserDataStore> {
|
||||
|
@ -91,10 +89,8 @@ export class ServerVariablesInitializer {
|
|||
new MailSenderBuilder(Nodemailer);
|
||||
const notifier = NotifierFactory.build(
|
||||
config.notifier, mailSenderBuilder);
|
||||
const accessController = new AccessController(
|
||||
config.access_control, deps.winston);
|
||||
const totpHandler = new TotpHandler(
|
||||
deps.speakeasy);
|
||||
const authorizer = new Authorizer(config.access_control, deps.winston);
|
||||
const totpHandler = new TotpHandler(deps.speakeasy);
|
||||
const usersDatabase = this.createUsersDatabase(
|
||||
config, deps);
|
||||
|
||||
|
@ -104,7 +100,7 @@ export class ServerVariablesInitializer {
|
|||
config.regulation.find_time, config.regulation.ban_time);
|
||||
|
||||
const variables: ServerVariables = {
|
||||
accessController: accessController,
|
||||
authorizer: authorizer,
|
||||
config: config,
|
||||
usersDatabase: usersDatabase,
|
||||
logger: requestLogger,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { ServerVariables } from "./ServerVariables";
|
|||
|
||||
import { Configuration } from "./configuration/schema/Configuration";
|
||||
import { IUsersDatabaseStub } from "./authentication/backends/IUsersDatabaseStub.spec";
|
||||
import { AccessControllerStub } from "./access_control/AccessControllerStub.spec";
|
||||
import { AuthorizerStub } from "./authorization/AuthorizerStub.spec";
|
||||
import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec";
|
||||
import { NotifierStub } from "./notifiers/NotifierStub.spec";
|
||||
import { RegulatorStub } from "./regulation/RegulatorStub.spec";
|
||||
|
@ -11,7 +11,7 @@ import { UserDataStoreStub } from "./storage/UserDataStoreStub.spec";
|
|||
import { U2fHandlerStub } from "./authentication/u2f/U2fHandlerStub.spec";
|
||||
|
||||
export interface ServerVariablesMock {
|
||||
accessController: AccessControllerStub;
|
||||
authorizer: AuthorizerStub;
|
||||
config: Configuration;
|
||||
usersDatabase: IUsersDatabaseStub;
|
||||
logger: RequestLoggerStub;
|
||||
|
@ -25,12 +25,9 @@ export interface ServerVariablesMock {
|
|||
export class ServerVariablesMockBuilder {
|
||||
static build(enableLogging?: boolean): { variables: ServerVariables, mocks: ServerVariablesMock} {
|
||||
const mocks: ServerVariablesMock = {
|
||||
accessController: new AccessControllerStub(),
|
||||
authorizer: new AuthorizerStub(),
|
||||
config: {
|
||||
access_control: {},
|
||||
authentication_methods: {
|
||||
default_method: "two_factor"
|
||||
},
|
||||
totp: {
|
||||
issuer: "authelia.com"
|
||||
},
|
||||
|
@ -71,7 +68,7 @@ export class ServerVariablesMockBuilder {
|
|||
u2f: new U2fHandlerStub()
|
||||
};
|
||||
const vars: ServerVariables = {
|
||||
accessController: mocks.accessController,
|
||||
authorizer: mocks.authorizer,
|
||||
config: mocks.config,
|
||||
usersDatabase: mocks.usersDatabase,
|
||||
logger: mocks.logger,
|
||||
|
|
|
@ -1,367 +0,0 @@
|
|||
|
||||
import Assert = require("assert");
|
||||
import winston = require("winston");
|
||||
import { AccessController } from "./AccessController";
|
||||
import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration";
|
||||
|
||||
describe("access_control/AccessController", function () {
|
||||
let accessController: AccessController;
|
||||
let configuration: ACLConfiguration;
|
||||
|
||||
describe("configuration is null", function() {
|
||||
it("should allow access to anything, anywhere for anybody", function() {
|
||||
configuration = undefined;
|
||||
accessController = new AccessController(configuration, winston);
|
||||
|
||||
Assert(accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1", "group2"]));
|
||||
Assert(accessController.isAccessAllowed("home.example.com", "/abc", "user1", ["group1", "group2"]));
|
||||
Assert(accessController.isAccessAllowed("home.example.com", "/", "user2", ["group1", "group2"]));
|
||||
Assert(accessController.isAccessAllowed("admin.example.com", "/", "user3", ["group3"]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("configuration is not null", function () {
|
||||
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 () {
|
||||
configuration.default_policy = "deny";
|
||||
});
|
||||
|
||||
it("should deny access when no rule is provided", function () {
|
||||
Assert(!accessController.isAccessAllowed("home.example.com", "/", "user1", ["group1"]));
|
||||
});
|
||||
|
||||
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 any rules", function () {
|
||||
it("should control access when any 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 'any' 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 'any' 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"]));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,111 +0,0 @@
|
|||
|
||||
import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration";
|
||||
import { IAccessController } from "./IAccessController";
|
||||
import { Winston } from "../../../types/Dependencies";
|
||||
import { MultipleDomainMatcher } from "./MultipleDomainMatcher";
|
||||
|
||||
|
||||
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 MultipleDomainMatcher.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 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 {
|
||||
if (!this.configuration) return true;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
import Sinon = require("sinon");
|
||||
import { IAccessController } from "./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);
|
||||
}
|
||||
}
|
|
@ -1,4 +0,0 @@
|
|||
|
||||
export interface IAccessController {
|
||||
isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean;
|
||||
}
|
5
server/src/lib/authentication/Level.ts
Normal file
5
server/src/lib/authentication/Level.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export enum Level {
|
||||
NOT_AUTHENTICATED = 0,
|
||||
ONE_FACTOR = 1,
|
||||
TWO_FACTOR = 2
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import { MethodCalculator } from "./MethodCalculator";
|
||||
import { AuthenticationMethodsConfiguration }
|
||||
from "../configuration/schema/AuthenticationMethodsConfiguration";
|
||||
import Assert = require("assert");
|
||||
|
||||
describe("authentication/MethodCalculator", function () {
|
||||
describe("test compute method", function () {
|
||||
it("should return default method when sub domain not overriden",
|
||||
function () {
|
||||
const options1: AuthenticationMethodsConfiguration = {
|
||||
default_method: "two_factor",
|
||||
per_subdomain_methods: {}
|
||||
};
|
||||
const options2: AuthenticationMethodsConfiguration = {
|
||||
default_method: "single_factor",
|
||||
per_subdomain_methods: {}
|
||||
};
|
||||
Assert.equal(MethodCalculator.compute(options1, "www.example.com"),
|
||||
"two_factor");
|
||||
Assert.equal(MethodCalculator.compute(options2, "www.example.com"),
|
||||
"single_factor");
|
||||
});
|
||||
|
||||
it("should return overridden method when sub domain method is defined",
|
||||
function () {
|
||||
const options1: AuthenticationMethodsConfiguration = {
|
||||
default_method: "two_factor",
|
||||
per_subdomain_methods: {
|
||||
"www.example.com": "single_factor"
|
||||
}
|
||||
};
|
||||
Assert.equal(MethodCalculator.compute(options1, "www.example.com"),
|
||||
"single_factor");
|
||||
Assert.equal(MethodCalculator.compute(options1, "home.example.com"),
|
||||
"two_factor");
|
||||
});
|
||||
});
|
||||
|
||||
describe("test isSingleFactorOnlyMode method", function () {
|
||||
it("should return true when default domains and all domains are single_factor",
|
||||
function () {
|
||||
const options: AuthenticationMethodsConfiguration = {
|
||||
default_method: "single_factor",
|
||||
per_subdomain_methods: {
|
||||
"www.example.com": "single_factor"
|
||||
}
|
||||
};
|
||||
Assert(MethodCalculator.isSingleFactorOnlyMode(options));
|
||||
});
|
||||
|
||||
it("should return false when default domains is single_factor and at least one sub-domain is two_factor", function () {
|
||||
const options: AuthenticationMethodsConfiguration = {
|
||||
default_method: "single_factor",
|
||||
per_subdomain_methods: {
|
||||
"www.example.com": "two_factor",
|
||||
"home.example.com": "single_factor"
|
||||
}
|
||||
};
|
||||
Assert(!MethodCalculator.isSingleFactorOnlyMode(options));
|
||||
});
|
||||
|
||||
it("should return false when default domains is two_factor", function () {
|
||||
const options: AuthenticationMethodsConfiguration = {
|
||||
default_method: "two_factor",
|
||||
per_subdomain_methods: {
|
||||
"www.example.com": "single_factor",
|
||||
"home.example.com": "single_factor"
|
||||
}
|
||||
};
|
||||
Assert(!MethodCalculator.isSingleFactorOnlyMode(options));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,40 +0,0 @@
|
|||
import {
|
||||
AuthenticationMethod,
|
||||
AuthenticationMethodsConfiguration
|
||||
} from "../configuration/schema/AuthenticationMethodsConfiguration";
|
||||
|
||||
function computeIsSingleFactorOnlyMode(
|
||||
configuration: AuthenticationMethodsConfiguration): boolean {
|
||||
if (!configuration)
|
||||
return false;
|
||||
|
||||
const method: AuthenticationMethod = configuration.default_method;
|
||||
if (configuration.default_method == "two_factor")
|
||||
return false;
|
||||
|
||||
if (configuration.per_subdomain_methods) {
|
||||
for (const key in configuration.per_subdomain_methods) {
|
||||
const method = configuration.per_subdomain_methods[key];
|
||||
if (method == "two_factor")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export class MethodCalculator {
|
||||
static compute(config: AuthenticationMethodsConfiguration, subDomain: string)
|
||||
: AuthenticationMethod {
|
||||
if (config
|
||||
&& config.per_subdomain_methods
|
||||
&& subDomain in config.per_subdomain_methods) {
|
||||
return config.per_subdomain_methods[subDomain];
|
||||
}
|
||||
return config.default_method;
|
||||
}
|
||||
|
||||
static isSingleFactorOnlyMode(config: AuthenticationMethodsConfiguration)
|
||||
: boolean {
|
||||
return computeIsSingleFactorOnlyMode(config);
|
||||
}
|
||||
}
|
372
server/src/lib/authorization/Authorizer.spec.ts
Normal file
372
server/src/lib/authorization/Authorizer.spec.ts
Normal file
|
@ -0,0 +1,372 @@
|
|||
|
||||
import Assert = require("assert");
|
||||
import winston = require("winston");
|
||||
import { Authorizer } from "./Authorizer";
|
||||
import { ACLConfiguration, ACLRule } from "../configuration/schema/AclConfiguration";
|
||||
import { Level } from "./Level";
|
||||
|
||||
describe("authorization/Authorizer", function () {
|
||||
let authorizer: Authorizer;
|
||||
let configuration: ACLConfiguration;
|
||||
|
||||
describe("configuration is null", function() {
|
||||
it("should allow access to anything, anywhere for anybody", function() {
|
||||
configuration = undefined;
|
||||
authorizer = new Authorizer(configuration, winston);
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS);
|
||||
});
|
||||
});
|
||||
|
||||
describe("configuration is not null", function () {
|
||||
beforeEach(function () {
|
||||
configuration = {
|
||||
default_policy: "deny",
|
||||
rules: []
|
||||
};
|
||||
authorizer = new Authorizer(configuration, winston);
|
||||
});
|
||||
|
||||
describe("check access control with default policy to deny", function () {
|
||||
beforeEach(function () {
|
||||
configuration.default_policy = "deny";
|
||||
});
|
||||
|
||||
it("should deny access when no rule is provided", function () {
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should control access when multiple domain matcher is provided", function () {
|
||||
configuration.rules = [{
|
||||
domain: "*.mail.example.com",
|
||||
policy: "two_factor",
|
||||
subject: "user:user1",
|
||||
resources: [".*"]
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should allow access to all resources when resources is not provided", function () {
|
||||
configuration.rules = [{
|
||||
domain: "*.mail.example.com",
|
||||
policy: "two_factor",
|
||||
subject: "user:user1"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
describe("check user rules", function () {
|
||||
it("should allow access when user has a matching allowing rule", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: [".*"],
|
||||
subject: "user:user1"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should deny to other users", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: [".*"],
|
||||
subject: "user:user1"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should allow user access only to specific resources", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["/private/.*", "^/begin", "/end$"],
|
||||
subject: "user:user1"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should allow access to multiple domains", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: [".*"],
|
||||
subject: "user:user1"
|
||||
}, {
|
||||
domain: "home1.example.com",
|
||||
policy: "one_factor",
|
||||
resources: [".*"],
|
||||
subject: "user:user1"
|
||||
}, {
|
||||
domain: "home2.example.com",
|
||||
policy: "deny",
|
||||
resources: [".*"],
|
||||
subject: "user:user1"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should apply rules in order", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "one_factor",
|
||||
resources: ["/my/private/resource"],
|
||||
subject: "user:user1"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/my/private/.*"],
|
||||
subject: "user:user1"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/my/.*"],
|
||||
subject: "user:user1"
|
||||
}];
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR);
|
||||
});
|
||||
});
|
||||
|
||||
describe("check group rules", function () {
|
||||
it("should allow access when user is in group having a matching allowing rule", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/$"],
|
||||
subject: "group:group1"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "one_factor",
|
||||
resources: ["^/test$"],
|
||||
subject: "group:group2"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/private$"],
|
||||
subject: "group:group2"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"},
|
||||
{user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"},
|
||||
{user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"},
|
||||
{user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"},
|
||||
{user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("check any rules", function () {
|
||||
it("should control access when any rules are defined", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "bypass",
|
||||
resources: ["^/public$"]
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/private$"]
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"},
|
||||
{user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"},
|
||||
{user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"},
|
||||
{user: "user4", groups: ["group5"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"},
|
||||
{user: "user4", groups: ["group5"]}), Level.DENY);
|
||||
});
|
||||
});
|
||||
|
||||
describe("check access control with default policy to allow", function () {
|
||||
beforeEach(function () {
|
||||
configuration.default_policy = "bypass";
|
||||
});
|
||||
|
||||
it("should allow access to anything when no rule is provided", function () {
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS);
|
||||
});
|
||||
|
||||
it("should deny access to one resource when defined", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["/test"],
|
||||
subject: "user:user1"
|
||||
}];
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS);
|
||||
});
|
||||
});
|
||||
|
||||
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.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/public$", "^/$"]
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: [".*"],
|
||||
subject: "group:admins"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/private/?.*"],
|
||||
subject: "group:admin-private"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/private/john$"],
|
||||
subject: "user:john"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/private/harry"],
|
||||
subject: "user:harry"
|
||||
}];
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR);
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR);
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
});
|
||||
|
||||
it("should allow when allowed at group level and denied at user level", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/dev/bob$"],
|
||||
subject: "user:john"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/dev/?.*$"],
|
||||
subject: "group:dev"
|
||||
}];
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should allow access when allowed at 'any' level and denied at user level", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/dev/bob$"],
|
||||
subject: "user:john"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/dev/?.*$"]
|
||||
}];
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY);
|
||||
});
|
||||
|
||||
it("should allow access when allowed at 'any' level and denied at group level", function () {
|
||||
configuration.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/dev/bob$"],
|
||||
subject: "group:dev"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/dev/?.*$"]
|
||||
}];
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY);
|
||||
});
|
||||
|
||||
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.rules = [{
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/dev/?.*$"],
|
||||
subject: "user:john"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "deny",
|
||||
resources: ["^/dev/bob$"],
|
||||
subject: "group:dev"
|
||||
}, {
|
||||
domain: "home.example.com",
|
||||
policy: "two_factor",
|
||||
resources: ["^/dev/?.*$"]
|
||||
}];
|
||||
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
85
server/src/lib/authorization/Authorizer.ts
Normal file
85
server/src/lib/authorization/Authorizer.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
|
||||
import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration";
|
||||
import { IAuthorizer } from "./IAuthorizer";
|
||||
import { Winston } from "../../../types/Dependencies";
|
||||
import { MultipleDomainMatcher } from "./MultipleDomainMatcher";
|
||||
import { Level } from "./Level";
|
||||
import { Object } from "./Object";
|
||||
import { Subject } from "./Subject";
|
||||
|
||||
function MatchDomain(actualDomain: string) {
|
||||
return function (rule: ACLRule): boolean {
|
||||
return MultipleDomainMatcher.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 MatchSubject(subject: Subject) {
|
||||
return (rule: ACLRule) => {
|
||||
// If no subject, matches anybody
|
||||
if (!rule.subject) return true;
|
||||
|
||||
if (rule.subject.startsWith("user:")) {
|
||||
const ruleUser = rule.subject.split(":")[1];
|
||||
if (subject.user == ruleUser) return true;
|
||||
}
|
||||
|
||||
if (rule.subject.startsWith("group:")) {
|
||||
const ruleGroup = rule.subject.split(":")[1];
|
||||
if (subject.groups.indexOf(ruleGroup) > -1) return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
}
|
||||
|
||||
export class Authorizer implements IAuthorizer {
|
||||
private logger: Winston;
|
||||
private readonly configuration: ACLConfiguration;
|
||||
|
||||
constructor(configuration: ACLConfiguration, logger_: Winston) {
|
||||
this.logger = logger_;
|
||||
this.configuration = configuration;
|
||||
}
|
||||
|
||||
private getMatchingRules(object: Object, subject: Subject): ACLRule[] {
|
||||
const rules = this.configuration.rules;
|
||||
if (!rules) return [];
|
||||
return rules
|
||||
.filter(MatchDomain(object.domain))
|
||||
.filter(MatchResource(object.resource))
|
||||
.filter(MatchSubject(subject));
|
||||
}
|
||||
|
||||
private ruleToLevel(policy: string): Level {
|
||||
if (policy == "bypass") {
|
||||
return Level.BYPASS;
|
||||
} else if (policy == "one_factor") {
|
||||
return Level.ONE_FACTOR;
|
||||
} else if (policy == "two_factor") {
|
||||
return Level.TWO_FACTOR;
|
||||
}
|
||||
return Level.DENY;
|
||||
}
|
||||
|
||||
authorization(object: Object, subject: Subject): Level {
|
||||
if (!this.configuration) return Level.BYPASS;
|
||||
|
||||
const rules = this.getMatchingRules(object, subject);
|
||||
|
||||
return (rules.length > 0)
|
||||
? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule
|
||||
: this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy
|
||||
}
|
||||
}
|
17
server/src/lib/authorization/AuthorizerStub.spec.ts
Normal file
17
server/src/lib/authorization/AuthorizerStub.spec.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import Sinon = require("sinon");
|
||||
import { IAuthorizer } from "./IAuthorizer";
|
||||
import { Level } from "./Level";
|
||||
import { Object } from "./Object";
|
||||
import { Subject } from "./Subject";
|
||||
|
||||
export class AuthorizerStub implements IAuthorizer {
|
||||
authorizationMock: Sinon.SinonStub;
|
||||
|
||||
constructor() {
|
||||
this.authorizationMock = Sinon.stub();
|
||||
}
|
||||
|
||||
authorization(object: Object, subject: Subject): Level {
|
||||
return this.authorizationMock(object, subject);
|
||||
}
|
||||
}
|
7
server/src/lib/authorization/IAuthorizer.ts
Normal file
7
server/src/lib/authorization/IAuthorizer.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { Level } from "./Level";
|
||||
import { Subject } from "./Subject";
|
||||
import { Object } from "./Object";
|
||||
|
||||
export interface IAuthorizer {
|
||||
authorization(object: Object, subject: Subject): Level;
|
||||
}
|
6
server/src/lib/authorization/Level.ts
Normal file
6
server/src/lib/authorization/Level.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
export enum Level {
|
||||
BYPASS = 0,
|
||||
ONE_FACTOR = 1,
|
||||
TWO_FACTOR = 2,
|
||||
DENY = 3
|
||||
}
|
5
server/src/lib/authorization/Object.ts
Normal file
5
server/src/lib/authorization/Object.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
export interface Object {
|
||||
domain: string;
|
||||
resource: string;
|
||||
}
|
5
server/src/lib/authorization/Subject.ts
Normal file
5
server/src/lib/authorization/Subject.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
|
||||
export interface Subject {
|
||||
user: string;
|
||||
groups: string[];
|
||||
}
|
|
@ -125,32 +125,26 @@ describe("configuration/ConfigurationParser", function () {
|
|||
const userConfig = buildYamlConfig();
|
||||
userConfig.access_control = {
|
||||
default_policy: "deny",
|
||||
any: [{
|
||||
rules: [{
|
||||
domain: "www.example.com",
|
||||
policy: "two_factor",
|
||||
subject: "user:user"
|
||||
}, {
|
||||
domain: "public.example.com",
|
||||
policy: "allow"
|
||||
}],
|
||||
users: {
|
||||
"user": [{
|
||||
domain: "www.example.com",
|
||||
policy: "allow"
|
||||
}]
|
||||
},
|
||||
groups: {}
|
||||
policy: "two_factor"
|
||||
}]
|
||||
};
|
||||
const config = ConfigurationParser.parse(userConfig);
|
||||
Assert.deepEqual(config.access_control, {
|
||||
default_policy: "deny",
|
||||
any: [{
|
||||
rules: [{
|
||||
domain: "www.example.com",
|
||||
policy: "two_factor",
|
||||
subject: "user:user"
|
||||
}, {
|
||||
domain: "public.example.com",
|
||||
policy: "allow"
|
||||
}],
|
||||
users: {
|
||||
"user": [{
|
||||
domain: "www.example.com",
|
||||
policy: "allow"
|
||||
}]
|
||||
},
|
||||
groups: {}
|
||||
policy: "two_factor"
|
||||
}]
|
||||
} as ACLConfiguration);
|
||||
});
|
||||
|
||||
|
@ -160,10 +154,8 @@ describe("configuration/ConfigurationParser", function () {
|
|||
userConfig.access_control = {} as any;
|
||||
const config = ConfigurationParser.parse(userConfig);
|
||||
Assert.deepEqual(config.access_control, {
|
||||
default_policy: "allow",
|
||||
any: [],
|
||||
users: {},
|
||||
groups: {}
|
||||
default_policy: "bypass",
|
||||
rules: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,9 +11,7 @@ describe("configuration/SessionConfigurationBuilder", function () {
|
|||
const configuration: Configuration = {
|
||||
access_control: {
|
||||
default_policy: "deny",
|
||||
any: [],
|
||||
users: {},
|
||||
groups: {}
|
||||
rules: []
|
||||
},
|
||||
totp: {
|
||||
issuer: "authelia.com"
|
||||
|
@ -54,10 +52,6 @@ describe("configuration/SessionConfigurationBuilder", function () {
|
|||
local: {
|
||||
in_memory: true
|
||||
}
|
||||
},
|
||||
authentication_methods: {
|
||||
default_method: "two_factor",
|
||||
per_subdomain_methods: {}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -4,11 +4,31 @@ import Assert = require("assert");
|
|||
describe("configuration/schema/AclConfiguration", function() {
|
||||
it("should complete ACLConfiguration", function() {
|
||||
const configuration: ACLConfiguration = {};
|
||||
const newConfiguration = complete(configuration);
|
||||
const [newConfiguration, errors] = complete(configuration);
|
||||
|
||||
Assert.deepEqual(newConfiguration.default_policy, "allow");
|
||||
Assert.deepEqual(newConfiguration.any, []);
|
||||
Assert.deepEqual(newConfiguration.groups, {});
|
||||
Assert.deepEqual(newConfiguration.users, {});
|
||||
Assert.deepEqual(newConfiguration.default_policy, "bypass");
|
||||
Assert.deepEqual(newConfiguration.rules, []);
|
||||
});
|
||||
|
||||
it("should return errors when subject is not good", function() {
|
||||
const configuration: ACLConfiguration = {
|
||||
default_policy: "deny",
|
||||
rules: [{
|
||||
domain: "dev.example.com",
|
||||
subject: "user:abc",
|
||||
policy: "bypass"
|
||||
}, {
|
||||
domain: "dev.example.com",
|
||||
subject: "user:def",
|
||||
policy: "bypass"
|
||||
}, {
|
||||
domain: "dev.example.com",
|
||||
subject: "badkey:abc",
|
||||
policy: "bypass"
|
||||
}]
|
||||
};
|
||||
const [newConfiguration, errors] = complete(configuration);
|
||||
|
||||
Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]);
|
||||
});
|
||||
});
|
|
@ -1,42 +1,41 @@
|
|||
|
||||
export type ACLPolicy = "deny" | "allow";
|
||||
export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor";
|
||||
|
||||
export type ACLRule = {
|
||||
domain: string;
|
||||
policy: ACLPolicy;
|
||||
resources?: string[];
|
||||
subject?: string;
|
||||
policy: ACLPolicy;
|
||||
};
|
||||
|
||||
export type ACLDefaultRules = ACLRule[];
|
||||
export type ACLGroupsRules = { [group: string]: ACLRule[]; };
|
||||
export type ACLUsersRules = { [user: string]: ACLRule[]; };
|
||||
|
||||
export interface ACLConfiguration {
|
||||
default_policy?: ACLPolicy;
|
||||
any?: ACLDefaultRules;
|
||||
groups?: ACLGroupsRules;
|
||||
users?: ACLUsersRules;
|
||||
rules?: ACLRule[];
|
||||
}
|
||||
|
||||
export function complete(configuration: ACLConfiguration): ACLConfiguration {
|
||||
export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] {
|
||||
const newConfiguration: ACLConfiguration = (configuration)
|
||||
? JSON.parse(JSON.stringify(configuration)) : {};
|
||||
|
||||
if (!newConfiguration.default_policy) {
|
||||
newConfiguration.default_policy = "allow";
|
||||
newConfiguration.default_policy = "bypass";
|
||||
}
|
||||
|
||||
if (!newConfiguration.any) {
|
||||
newConfiguration.any = [];
|
||||
if (!newConfiguration.rules) {
|
||||
newConfiguration.rules = [];
|
||||
}
|
||||
|
||||
if (!newConfiguration.groups) {
|
||||
newConfiguration.groups = {};
|
||||
if (newConfiguration.rules.length > 0) {
|
||||
const errors: string[] = [];
|
||||
newConfiguration.rules.forEach((r, idx) => {
|
||||
if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) {
|
||||
errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`);
|
||||
}
|
||||
});
|
||||
if (errors.length > 0) {
|
||||
return [newConfiguration, errors];
|
||||
}
|
||||
}
|
||||
|
||||
if (!newConfiguration.users) {
|
||||
newConfiguration.users = {};
|
||||
}
|
||||
|
||||
return newConfiguration;
|
||||
return [newConfiguration, []];
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import Assert = require("assert");
|
||||
import { AuthenticationMethodsConfiguration, complete } from "./AuthenticationMethodsConfiguration";
|
||||
|
||||
describe("configuration/schema/AuthenticationMethodsConfiguration", function() {
|
||||
it("should ensure at least one key is provided", function() {
|
||||
const configuration: AuthenticationMethodsConfiguration = {};
|
||||
const newConfiguration = complete(configuration);
|
||||
|
||||
Assert.deepEqual(newConfiguration.default_method, "two_factor");
|
||||
Assert.deepEqual(newConfiguration.per_subdomain_methods, []);
|
||||
});
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
export type AuthenticationMethod = "two_factor" | "single_factor";
|
||||
export type AuthenticationMethodPerSubdomain = { [subdomain: string]: AuthenticationMethod };
|
||||
|
||||
export interface AuthenticationMethodsConfiguration {
|
||||
default_method?: AuthenticationMethod;
|
||||
per_subdomain_methods?: AuthenticationMethodPerSubdomain;
|
||||
}
|
||||
|
||||
export function complete(configuration: AuthenticationMethodsConfiguration): AuthenticationMethodsConfiguration {
|
||||
const newConfiguration: AuthenticationMethodsConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {};
|
||||
|
||||
if (!newConfiguration.default_method) {
|
||||
newConfiguration.default_method = "two_factor";
|
||||
}
|
||||
|
||||
if (!newConfiguration.per_subdomain_methods) {
|
||||
newConfiguration.per_subdomain_methods = {};
|
||||
}
|
||||
|
||||
return newConfiguration;
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration";
|
||||
import { AuthenticationMethodsConfiguration, complete as AuthenticationMethodsConfigurationComplete } from "./AuthenticationMethodsConfiguration";
|
||||
import { AuthenticationBackendConfiguration, complete as AuthenticationBackendComplete } from "./AuthenticationBackendConfiguration";
|
||||
import { NotifierConfiguration, complete as NotifierConfigurationComplete } from "./NotifierConfiguration";
|
||||
import { RegulationConfiguration, complete as RegulationConfigurationComplete } from "./RegulationConfiguration";
|
||||
import { SessionConfiguration, complete as SessionConfigurationComplete } from "./SessionConfiguration";
|
||||
import { StorageConfiguration, complete as StorageConfigurationComplete } from "./StorageConfiguration";
|
||||
import { TotpConfiguration, complete as TotpConfigurationComplete } from "./TotpConfiguration";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
|
||||
export interface Configuration {
|
||||
access_control?: ACLConfiguration;
|
||||
authentication_backend: AuthenticationBackendConfiguration;
|
||||
authentication_methods?: AuthenticationMethodsConfiguration;
|
||||
default_redirection_url?: string;
|
||||
logs_level?: string;
|
||||
notifier?: NotifierConfiguration;
|
||||
|
@ -30,9 +27,13 @@ export function complete(
|
|||
JSON.stringify(configuration));
|
||||
const errors: string[] = [];
|
||||
|
||||
newConfiguration.access_control =
|
||||
AclConfigurationComplete(
|
||||
newConfiguration.access_control);
|
||||
const [acls, aclsErrors] = AclConfigurationComplete(
|
||||
newConfiguration.access_control);
|
||||
|
||||
newConfiguration.access_control = acls;
|
||||
if (aclsErrors.length > 0) {
|
||||
errors.concat(aclsErrors);
|
||||
}
|
||||
|
||||
const [backend, error] =
|
||||
AuthenticationBackendComplete(
|
||||
|
@ -41,25 +42,14 @@ export function complete(
|
|||
if (error) errors.push(error);
|
||||
newConfiguration.authentication_backend = backend;
|
||||
|
||||
newConfiguration.authentication_methods =
|
||||
AuthenticationMethodsConfigurationComplete(
|
||||
newConfiguration.authentication_methods);
|
||||
|
||||
if (!newConfiguration.logs_level) {
|
||||
newConfiguration.logs_level = "info";
|
||||
}
|
||||
|
||||
// In single factor mode, notifier section is optional.
|
||||
if (!MethodCalculator.isSingleFactorOnlyMode(
|
||||
newConfiguration.authentication_methods) ||
|
||||
newConfiguration.notifier) {
|
||||
|
||||
const [notifier, error] = NotifierConfigurationComplete(
|
||||
newConfiguration.notifier);
|
||||
newConfiguration.notifier = notifier;
|
||||
|
||||
if (error) errors.push(error);
|
||||
}
|
||||
const [notifier, notifierError] = NotifierConfigurationComplete(
|
||||
newConfiguration.notifier);
|
||||
newConfiguration.notifier = notifier;
|
||||
if (notifierError) errors.push(notifierError);
|
||||
|
||||
if (!newConfiguration.port) {
|
||||
newConfiguration.port = 8080;
|
||||
|
|
|
@ -6,12 +6,12 @@ describe("configuration/schema/NotifierConfiguration", function() {
|
|||
const configuration: NotifierConfiguration = {};
|
||||
const [newConfiguration, error] = complete(configuration);
|
||||
|
||||
Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"})
|
||||
Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"});
|
||||
});
|
||||
|
||||
it("should ensure correct key is provided", function() {
|
||||
const configuration = {
|
||||
abc: 'badvalue'
|
||||
abc: "badvalue"
|
||||
};
|
||||
const [newConfiguration, error] = complete(configuration as any);
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
|
||||
import express = require("express");
|
||||
import objectPath = require("object-path");
|
||||
import Endpoints = require("../../../../../shared/api");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
|
@ -8,6 +7,7 @@ import Constants = require("../../../../../shared/constants");
|
|||
import Util = require("util");
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { SafeRedirector } from "../../utils/SafeRedirection";
|
||||
import { Level } from "../../authentication/Level";
|
||||
|
||||
function getRedirectParam(
|
||||
req: express.Request) {
|
||||
|
@ -59,15 +59,13 @@ export default function (
|
|||
return function (req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
if (authSession.first_factor) {
|
||||
if (authSession.second_factor)
|
||||
redirectToService(req, res, redirector);
|
||||
else
|
||||
redirectToSecondFactorPage(req, res);
|
||||
resolve();
|
||||
return;
|
||||
if (authSession.authentication_level == Level.ONE_FACTOR) {
|
||||
redirectToSecondFactorPage(req, res);
|
||||
} else if (authSession.authentication_level == Level.TWO_FACTOR) {
|
||||
redirectToService(req, res, redirector);
|
||||
} else {
|
||||
renderFirstFactor(res);
|
||||
}
|
||||
renderFirstFactor(res);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@ import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler
|
|||
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
|
||||
import Endpoints = require("../../../../../shared/api");
|
||||
import AuthenticationRegulatorMock = require("../../regulation/RegulatorStub.spec");
|
||||
import { AccessControllerStub } from "../../access_control/AccessControllerStub.spec";
|
||||
import ExpressMock = require("../../stubs/express.spec");
|
||||
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../ServerVariablesMockBuilder.spec";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
|
@ -29,7 +28,7 @@ describe("routes/firstfactor/post", function () {
|
|||
mocks = s.mocks;
|
||||
vars = s.variables;
|
||||
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.authorizer.authorizationMock.returns(true);
|
||||
mocks.regulator.regulateStub.returns(BluebirdPromise.resolve());
|
||||
mocks.regulator.markStub.returns(BluebirdPromise.resolve());
|
||||
|
||||
|
|
|
@ -1,20 +1,18 @@
|
|||
|
||||
import Exceptions = require("../../Exceptions");
|
||||
import objectPath = require("object-path");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import express = require("express");
|
||||
import { AccessController } from "../../access_control/AccessController";
|
||||
import { Regulator } from "../../regulation/Regulator";
|
||||
import Endpoint = require("../../../../../shared/api");
|
||||
import ErrorReplies = require("../../ErrorReplies");
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
|
||||
import UserMessages = require("../../../../../shared/UserMessages");
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSession } from "../../../../types/AuthenticationSession";
|
||||
import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails";
|
||||
import { Level as AuthenticationLevel } from "../../authentication/Level";
|
||||
import { Level as AuthorizationLevel } from "../../authorization/Level";
|
||||
import { URLDecomposer } from "../../utils/URLDecomposer";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
return function (req: express.Request, res: express.Response)
|
||||
|
@ -50,21 +48,20 @@ export default function (vars: ServerVariables) {
|
|||
JSON.stringify(groupsAndEmails));
|
||||
authSession.userid = username;
|
||||
authSession.keep_me_logged_in = keepMeLoggedIn;
|
||||
authSession.first_factor = true;
|
||||
authSession.authentication_level = AuthenticationLevel.ONE_FACTOR;
|
||||
const redirectUrl: string = req.query[Constants.REDIRECT_QUERY_PARAM] !== "undefined"
|
||||
// Fuck, don't know why it is a string!
|
||||
? req.query[Constants.REDIRECT_QUERY_PARAM]
|
||||
: undefined;
|
||||
: "";
|
||||
|
||||
const emails: string[] = groupsAndEmails.emails;
|
||||
const groups: string[] = groupsAndEmails.groups;
|
||||
|
||||
const domain = DomainExtractor.fromUrl(redirectUrl);
|
||||
const redirectHost = (domain) ? domain : "";
|
||||
const authMethod = MethodCalculator.compute(
|
||||
vars.config.authentication_methods, redirectHost);
|
||||
vars.logger.debug(req, "Authentication method for \"%s\" is \"%s\"",
|
||||
redirectHost, authMethod);
|
||||
const decomposition = URLDecomposer.fromUrl(redirectUrl);
|
||||
const authorizationLevel = (decomposition)
|
||||
? vars.authorizer.authorization(
|
||||
{domain: decomposition.domain, resource: decomposition.path},
|
||||
{user: username, groups: groups})
|
||||
: AuthorizationLevel.TWO_FACTOR;
|
||||
|
||||
if (emails.length > 0)
|
||||
authSession.email = emails[0];
|
||||
|
@ -73,8 +70,8 @@ export default function (vars: ServerVariables) {
|
|||
vars.logger.debug(req, "Mark successful authentication to regulator.");
|
||||
vars.regulator.mark(username, true);
|
||||
|
||||
if (authMethod == "single_factor") {
|
||||
let newRedirectionUrl = redirectUrl;
|
||||
if (authorizationLevel <= AuthorizationLevel.ONE_FACTOR) {
|
||||
let newRedirectionUrl: string = redirectUrl;
|
||||
if (!newRedirectionUrl)
|
||||
newRedirectionUrl = Endpoint.LOGGED_IN;
|
||||
res.send({
|
||||
|
@ -82,7 +79,7 @@ export default function (vars: ServerVariables) {
|
|||
});
|
||||
vars.logger.debug(req, "Redirect to '%s'", redirectUrl);
|
||||
}
|
||||
else if (authMethod == "two_factor") {
|
||||
else {
|
||||
let newRedirectUrl = Endpoint.SECOND_FACTOR_GET;
|
||||
if (redirectUrl) {
|
||||
newRedirectUrl += "?" + Constants.REDIRECT_QUERY_PARAM + "="
|
||||
|
@ -93,9 +90,6 @@ export default function (vars: ServerVariables) {
|
|||
redirect: newRedirectUrl
|
||||
});
|
||||
}
|
||||
else {
|
||||
return BluebirdPromise.reject(new Error("Unknown authentication method for this domain."));
|
||||
}
|
||||
return BluebirdPromise.resolve();
|
||||
})
|
||||
.catch(Exceptions.LdapBindError, function (err: Error) {
|
||||
|
|
|
@ -9,6 +9,7 @@ import BluebirdPromise = require("bluebird");
|
|||
import ExpressMock = require("../../../stubs/express.spec");
|
||||
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../ServerVariablesMockBuilder.spec";
|
||||
import { ServerVariables } from "../../../ServerVariables";
|
||||
import { Level } from "../../../authentication/Level";
|
||||
|
||||
describe("routes/password-reset/form/post", function () {
|
||||
let req: ExpressMock.RequestMock;
|
||||
|
@ -59,8 +60,7 @@ describe("routes/password-reset/form/post", function () {
|
|||
authSession = AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
authSession.userid = "user";
|
||||
authSession.email = "user@example.com";
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = false;
|
||||
authSession.authentication_level = Level.ONE_FACTOR;
|
||||
});
|
||||
|
||||
describe("test reset password post", () => {
|
||||
|
@ -79,8 +79,7 @@ describe("routes/password-reset/form/post", function () {
|
|||
return AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
}).then(function (_authSession) {
|
||||
Assert.equal(res.status.getCall(0).args[0], 204);
|
||||
Assert.equal(_authSession.first_factor, false);
|
||||
Assert.equal(_authSession.second_factor, false);
|
||||
Assert.equal(_authSession.authentication_level, Level.NOT_AUTHENTICATED);
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,28 +31,8 @@ describe("routes/secondfactor/get", function () {
|
|||
};
|
||||
});
|
||||
|
||||
describe("test redirection", function () {
|
||||
it("should redirect to already logged in page if server is in single_factor mode", function () {
|
||||
vars.config.authentication_methods.default_method = "single_factor";
|
||||
return SecondFactorGet(vars)(req as any, res as any)
|
||||
.then(function () {
|
||||
Assert(res.redirect.calledWith(Endpoints.LOGGED_IN));
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
it("should redirect to already logged in page if user already authenticated", function () {
|
||||
vars.config.authentication_methods.default_method = "two_factor";
|
||||
req.session.auth.second_factor = true;
|
||||
return SecondFactorGet(vars)(req as any, res as any)
|
||||
.then(function () {
|
||||
Assert(res.redirect.calledWith(Endpoints.LOGGED_IN));
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
|
||||
describe("test rendering", function () {
|
||||
it("should render second factor page", function () {
|
||||
vars.config.authentication_methods.default_method = "two_factor";
|
||||
req.session.auth.second_factor = false;
|
||||
return SecondFactorGet(vars)(req as any, res as any)
|
||||
.then(function () {
|
||||
|
|
|
@ -4,7 +4,6 @@ import Endpoints = require("../../../../../shared/api");
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
|
||||
const TEMPLATE_NAME = "secondfactor";
|
||||
|
||||
|
@ -13,15 +12,7 @@ export default function (vars: ServerVariables) {
|
|||
: BluebirdPromise<void> {
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
const isSingleFactorMode: boolean = MethodCalculator.isSingleFactorOnlyMode(
|
||||
vars.config.authentication_methods);
|
||||
const authSession = AuthenticationSessionHandler.get(req, vars.logger);
|
||||
if (isSingleFactorMode
|
||||
|| (authSession.first_factor && authSession.second_factor)) {
|
||||
res.redirect(Endpoints.LOGGED_IN);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
res.render(TEMPLATE_NAME, {
|
||||
username: authSession.userid,
|
||||
|
|
|
@ -11,6 +11,7 @@ import { ServerVariables } from "../../../../ServerVariables";
|
|||
import ExpressMock = require("../../../../stubs/express.spec");
|
||||
import { UserDataStoreStub } from "../../../../storage/UserDataStoreStub.spec";
|
||||
import { ServerVariablesMock, ServerVariablesMockBuilder } from "../../../../ServerVariablesMockBuilder.spec";
|
||||
import { Level } from "../../../../authentication/Level";
|
||||
|
||||
describe("routes/secondfactor/totp/sign/post", function () {
|
||||
let req: ExpressMock.RequestMock;
|
||||
|
@ -46,8 +47,7 @@ describe("routes/secondfactor/totp/sign/post", function () {
|
|||
mocks.userDataStore.retrieveTOTPSecretStub.returns(BluebirdPromise.resolve(doc));
|
||||
authSession = AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
authSession.userid = "user";
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = false;
|
||||
authSession.authentication_level = Level.ONE_FACTOR;
|
||||
});
|
||||
|
||||
|
||||
|
@ -55,7 +55,7 @@ describe("routes/secondfactor/totp/sign/post", function () {
|
|||
mocks.totpHandler.validateStub.returns(true);
|
||||
return SignPost.default(vars)(req as any, res as any)
|
||||
.then(function () {
|
||||
Assert.equal(true, authSession.second_factor);
|
||||
Assert.equal(authSession.authentication_level, Level.TWO_FACTOR);
|
||||
return BluebirdPromise.resolve();
|
||||
});
|
||||
});
|
||||
|
@ -64,7 +64,7 @@ describe("routes/secondfactor/totp/sign/post", function () {
|
|||
mocks.totpHandler.validateStub.returns(false);
|
||||
return SignPost.default(vars)(req as any, res as any)
|
||||
.then(function () {
|
||||
Assert.equal(false, authSession.second_factor);
|
||||
Assert.notEqual(authSession.authentication_level, Level.TWO_FACTOR);
|
||||
Assert.equal(res.status.getCall(0).args[0], 200);
|
||||
Assert.deepEqual(res.send.getCall(0).args[0], {
|
||||
error: "Operation failed."
|
||||
|
|
|
@ -9,6 +9,7 @@ import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionH
|
|||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||
import { ServerVariables } from "../../../../ServerVariables";
|
||||
import { Level } from "../../../../authentication/Level";
|
||||
|
||||
const UNAUTHORIZED_MESSAGE = "Unauthorized access";
|
||||
|
||||
|
@ -30,7 +31,7 @@ export default function (vars: ServerVariables) {
|
|||
return Bluebird.reject(new Error("Invalid TOTP token."));
|
||||
|
||||
vars.logger.debug(req, "TOTP validation succeeded.");
|
||||
authSession.second_factor = true;
|
||||
authSession.authentication_level = Level.TWO_FACTOR;
|
||||
Redirect(vars)(req, res);
|
||||
return Bluebird.resolve();
|
||||
})
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../../../Ser
|
|||
import ExpressMock = require("../../../../stubs/express.spec");
|
||||
import U2FMock = require("../../../../stubs/u2f.spec");
|
||||
import U2f = require("u2f");
|
||||
import { Level } from "../../../../authentication/Level";
|
||||
|
||||
describe("routes/secondfactor/u2f/sign/post", function () {
|
||||
let req: ExpressMock.RequestMock;
|
||||
|
@ -29,8 +30,7 @@ describe("routes/secondfactor/u2f/sign/post", function () {
|
|||
req.session = {
|
||||
auth: {
|
||||
userid: "user",
|
||||
first_factor: true,
|
||||
second_factor: false,
|
||||
authentication_level: Level.ONE_FACTOR,
|
||||
identity_check: {
|
||||
challenge: "u2f-register",
|
||||
userid: "user"
|
||||
|
@ -72,7 +72,7 @@ describe("routes/secondfactor/u2f/sign/post", function () {
|
|||
};
|
||||
return U2FSignPost.default(vars)(req as any, res as any)
|
||||
.then(function () {
|
||||
Assert(req.session.auth.second_factor);
|
||||
Assert.equal(req.session.auth.authentication_level, Level.TWO_FACTOR);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { ServerVariables } from "../../../../ServerVariables";
|
|||
import { AuthenticationSessionHandler } from "../../../../AuthenticationSessionHandler";
|
||||
import UserMessages = require("../../../../../../../shared/UserMessages");
|
||||
import { AuthenticationSession } from "../../../../../../types/AuthenticationSession";
|
||||
import { Level } from "../../../../authentication/Level";
|
||||
|
||||
export default function (vars: ServerVariables) {
|
||||
function handler(req: express.Request, res: express.Response): BluebirdPromise<void> {
|
||||
|
@ -43,7 +44,7 @@ export default function (vars: ServerVariables) {
|
|||
if (objectPath.has(result, "errorCode"))
|
||||
return BluebirdPromise.reject(new Error("Error while signing"));
|
||||
vars.logger.info(req, "Successful authentication");
|
||||
authSession.second_factor = true;
|
||||
authSession.authentication_level = Level.TWO_FACTOR;
|
||||
redirect(vars)(req, res);
|
||||
return BluebirdPromise.resolve();
|
||||
})
|
||||
|
|
|
@ -2,19 +2,48 @@ import Express = require("express");
|
|||
import BluebirdPromise = require("bluebird");
|
||||
import Util = require("util");
|
||||
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import Exceptions = require("../../Exceptions");
|
||||
|
||||
export default function (req: Express.Request, vars: ServerVariables,
|
||||
domain: string, path: string, username: string, groups: string[]) {
|
||||
import { Level as AuthorizationLevel } from "../../authorization/Level";
|
||||
import { Level as AuthenticationLevel } from "../../authentication/Level";
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
|
||||
function isAuthorized(
|
||||
authorization: AuthorizationLevel,
|
||||
authentication: AuthenticationLevel): boolean {
|
||||
|
||||
if (authorization == AuthorizationLevel.BYPASS) {
|
||||
return true;
|
||||
} else if (authorization == AuthorizationLevel.ONE_FACTOR &&
|
||||
authentication >= AuthenticationLevel.ONE_FACTOR) {
|
||||
return true;
|
||||
} else if (authorization == AuthorizationLevel.TWO_FACTOR &&
|
||||
authentication >= AuthenticationLevel.TWO_FACTOR) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function (
|
||||
req: Express.Request,
|
||||
vars: ServerVariables,
|
||||
domain: string, resource: string,
|
||||
user: string, groups: string[],
|
||||
authenticationLevel: AuthenticationLevel) {
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
const isAllowed = vars.accessController
|
||||
.isAccessAllowed(domain, path, username, groups);
|
||||
const authorizationLevel = vars.authorizer
|
||||
.authorization({domain, resource}, {user, groups});
|
||||
|
||||
if (!isAllowed) {
|
||||
reject(new Exceptions.DomainAccessDenied(Util.format(
|
||||
"User '%s' does not have access to '%s'", username, domain)));
|
||||
if (!isAuthorized(authorizationLevel, authenticationLevel)) {
|
||||
if (authorizationLevel == AuthorizationLevel.DENY) {
|
||||
reject(new Exceptions.NotAuthorizedError(
|
||||
Util.format("User %s is not authorized to access %s%s", user, domain, resource)));
|
||||
return;
|
||||
}
|
||||
reject(new Exceptions.NotAuthenticatedError(Util.format(
|
||||
"User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource)));
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
|
|
|
@ -11,6 +11,8 @@ import { AuthenticationSession } from "../../../../types/AuthenticationSession";
|
|||
import ExpressMock = require("../../stubs/express.spec");
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { ServerVariablesMockBuilder, ServerVariablesMock } from "../../ServerVariablesMockBuilder.spec";
|
||||
import { Level } from "../../authentication/Level";
|
||||
import { Level as AuthorizationLevel } from "../../authorization/Level";
|
||||
|
||||
describe("routes/verify/get", function () {
|
||||
let req: ExpressMock.RequestMock;
|
||||
|
@ -35,14 +37,9 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
describe("with session cookie", function () {
|
||||
beforeEach(function () {
|
||||
vars.config.authentication_methods.default_method = "two_factor";
|
||||
});
|
||||
|
||||
it("should be already authenticated", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
authSession.authentication_level = Level.TWO_FACTOR;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
return VerifyGet.default(vars)(req as Express.Request, res as any)
|
||||
|
@ -74,7 +71,7 @@ describe("routes/verify/get", function () {
|
|||
|
||||
describe("given user tries to access a 2-factor endpoint", function () {
|
||||
before(function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
});
|
||||
|
||||
describe("given different cases of session", function () {
|
||||
|
@ -82,20 +79,7 @@ describe("routes/verify/get", function () {
|
|||
return test_non_authenticated_401({
|
||||
keep_me_logged_in: false,
|
||||
userid: "user",
|
||||
first_factor: true,
|
||||
second_factor: false,
|
||||
email: undefined,
|
||||
groups: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be authenticated when first factor is missing", function () {
|
||||
return test_non_authenticated_401({
|
||||
keep_me_logged_in: false,
|
||||
userid: "user",
|
||||
first_factor: false,
|
||||
second_factor: true,
|
||||
authentication_level: Level.ONE_FACTOR,
|
||||
email: undefined,
|
||||
groups: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
|
@ -106,20 +90,18 @@ describe("routes/verify/get", function () {
|
|||
return test_non_authenticated_401({
|
||||
keep_me_logged_in: false,
|
||||
userid: undefined,
|
||||
first_factor: true,
|
||||
second_factor: false,
|
||||
authentication_level: Level.TWO_FACTOR,
|
||||
email: undefined,
|
||||
groups: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
});
|
||||
});
|
||||
|
||||
it("should not be authenticated when first and second factor are missing", function () {
|
||||
it("should not be authenticated when level is insufficient", function () {
|
||||
return test_non_authenticated_401({
|
||||
keep_me_logged_in: false,
|
||||
userid: "user",
|
||||
first_factor: false,
|
||||
second_factor: false,
|
||||
authentication_level: Level.NOT_AUTHENTICATED,
|
||||
email: undefined,
|
||||
groups: [],
|
||||
last_activity_datetime: new Date().getTime()
|
||||
|
@ -131,16 +113,14 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
it("should not be authenticated when domain is not allowed for user", function () {
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.authentication_level = Level.TWO_FACTOR;
|
||||
authSession.userid = "myuser";
|
||||
req.headers["x-original-url"] = "https://test.example.com/";
|
||||
mocks.accessController.isAccessAllowedMock.returns(false);
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.DENY);
|
||||
|
||||
return test_unauthorized_403({
|
||||
keep_me_logged_in: false,
|
||||
first_factor: true,
|
||||
second_factor: true,
|
||||
authentication_level: Level.TWO_FACTOR,
|
||||
userid: "user",
|
||||
groups: ["group1", "group2"],
|
||||
email: undefined,
|
||||
|
@ -153,14 +133,11 @@ describe("routes/verify/get", function () {
|
|||
describe("given user tries to access a single factor endpoint", function () {
|
||||
beforeEach(function () {
|
||||
req.headers["x-original-url"] = "https://redirect.url/";
|
||||
mocks.config.authentication_methods.per_subdomain_methods = {
|
||||
"redirect.url": "single_factor"
|
||||
};
|
||||
});
|
||||
|
||||
it("should be authenticated when first factor is validated and second factor is not", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = true;
|
||||
it("should be authenticated when first factor is validated", function () {
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR);
|
||||
authSession.authentication_level = Level.ONE_FACTOR;
|
||||
authSession.userid = "user1";
|
||||
return VerifyGet.default(vars)(req as Express.Request, res as any)
|
||||
.then(function () {
|
||||
|
@ -169,9 +146,9 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
});
|
||||
|
||||
it("should be rejected with 401 when first factor is not validated", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
authSession.first_factor = false;
|
||||
it("should be rejected with 401 when not authenticated", function () {
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR);
|
||||
authSession.authentication_level = Level.NOT_AUTHENTICATED;
|
||||
return VerifyGet.default(vars)(req as Express.Request, res as any)
|
||||
.then(function () {
|
||||
Assert(res.status.calledWith(401));
|
||||
|
@ -182,11 +159,10 @@ describe("routes/verify/get", function () {
|
|||
describe("inactivity period", function () {
|
||||
it("should update last inactivity period on requests on /api/verify", function () {
|
||||
mocks.config.session.inactivity = 200000;
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
const currentTime = new Date().getTime() - 1000;
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.authentication_level = Level.TWO_FACTOR;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
|
@ -201,11 +177,10 @@ describe("routes/verify/get", function () {
|
|||
|
||||
it("should reset session when max inactivity period has been reached", function () {
|
||||
mocks.config.session.inactivity = 1;
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
const currentTime = new Date().getTime() - 1000;
|
||||
AuthenticationSessionHandler.reset(req as any);
|
||||
authSession.first_factor = true;
|
||||
authSession.second_factor = true;
|
||||
authSession.authentication_level = Level.TWO_FACTOR;
|
||||
authSession.userid = "myuser";
|
||||
authSession.groups = ["mygroup", "othergroup"];
|
||||
authSession.last_activity_datetime = currentTime;
|
||||
|
@ -214,8 +189,7 @@ describe("routes/verify/get", function () {
|
|||
return AuthenticationSessionHandler.get(req as any, vars.logger);
|
||||
})
|
||||
.then(function (authSession) {
|
||||
Assert.equal(authSession.first_factor, false);
|
||||
Assert.equal(authSession.second_factor, false);
|
||||
Assert.equal(authSession.authentication_level, Level.NOT_AUTHENTICATED);
|
||||
Assert.equal(authSession.userid, undefined);
|
||||
});
|
||||
});
|
||||
|
@ -224,8 +198,8 @@ describe("routes/verify/get", function () {
|
|||
|
||||
describe("response type 401 | 302", function() {
|
||||
it("should return error code 401", function() {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.rejects(new Error(
|
||||
"Invalid credentials"));
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
@ -238,8 +212,8 @@ describe("routes/verify/get", function () {
|
|||
|
||||
it("should redirect to provided redirection url", function() {
|
||||
const REDIRECT_URL = "http://redirection_url.com";
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.rejects(new Error(
|
||||
"Invalid credentials"));
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
@ -254,8 +228,8 @@ describe("routes/verify/get", function () {
|
|||
|
||||
describe("with basic auth", function () {
|
||||
it("should authenticate correctly", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.ONE_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.returns({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
|
@ -270,11 +244,12 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
it("should fail when endpoint is protected by two factors", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.config.authentication_methods.per_subdomain_methods = {
|
||||
"secret.example.com": "two_factor"
|
||||
};
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.config.access_control.rules = [{
|
||||
domain: "secret.example.com",
|
||||
policy: "two_factor"
|
||||
}];
|
||||
mocks.usersDatabase.checkUserPasswordStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
|
@ -287,8 +262,8 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
it("should fail when base64 token is not valid", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
|
@ -301,8 +276,8 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
it("should fail when base64 token has not format user:psswd", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
|
@ -315,8 +290,8 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
it("should fail when bad user password is provided", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(true);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.rejects(new Error(
|
||||
"Invalid credentials"));
|
||||
req.headers["proxy-authorization"] = "Basic am9objpwYXNzd29yZA==";
|
||||
|
@ -328,8 +303,8 @@ describe("routes/verify/get", function () {
|
|||
});
|
||||
|
||||
it("should fail when resource is restricted", function () {
|
||||
mocks.accessController.isAccessAllowedMock.returns(false);
|
||||
mocks.config.authentication_methods.default_method = "single_factor";
|
||||
mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR);
|
||||
mocks.config.access_control.default_policy = "one_factor";
|
||||
mocks.usersDatabase.checkUserPasswordStub.resolves({
|
||||
groups: ["mygroup", "othergroup"],
|
||||
});
|
||||
|
|
|
@ -72,10 +72,12 @@ export default function (vars: ServerVariables) {
|
|||
.then(setUserAndGroupsHeaders(res))
|
||||
.then(replyWith200(res))
|
||||
// The user is authenticated but has restricted access -> 403
|
||||
.catch(Exceptions.DomainAccessDenied, ErrorReplies
|
||||
.replyWithError403(req, res, vars.logger))
|
||||
.catch(Exceptions.NotAuthorizedError,
|
||||
ErrorReplies.replyWithError403(req, res, vars.logger))
|
||||
.catch(Exceptions.NotAuthenticatedError,
|
||||
ErrorReplies.replyWithError401(req, res, vars.logger))
|
||||
// The user is not yet authenticated -> 401
|
||||
.catch(function (err) {
|
||||
.catch((err) => {
|
||||
const redirectUrl = getRedirectParam(req);
|
||||
if (redirectUrl) {
|
||||
ErrorReplies.redirectTo(redirectUrl, req, res, vars.logger)(err);
|
||||
|
|
|
@ -4,31 +4,19 @@ import ObjectPath = require("object-path");
|
|||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import AccessControl from "./access_control";
|
||||
import { URLDecomposer } from "../../utils/URLDecomposer";
|
||||
import { Level } from "../../authentication/Level";
|
||||
|
||||
export default function (req: Express.Request, res: Express.Response,
|
||||
vars: ServerVariables, authorizationHeader: string)
|
||||
: BluebirdPromise<{ username: string, groups: string[] }> {
|
||||
let username: string;
|
||||
let domain: string;
|
||||
let originalUri: string;
|
||||
const uri = ObjectPath.get<Express.Request, string>(req, "headers.x-original-url");
|
||||
const urlDecomposition = URLDecomposer.fromUrl(uri);
|
||||
|
||||
return BluebirdPromise.resolve()
|
||||
.then(() => {
|
||||
const originalUrl = ObjectPath.get<Express.Request, string>(req, "headers.x-original-url");
|
||||
domain = DomainExtractor.fromUrl(originalUrl);
|
||||
originalUri =
|
||||
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
|
||||
const authenticationMethod =
|
||||
MethodCalculator.compute(vars.config.authentication_methods, domain);
|
||||
|
||||
if (authenticationMethod != "single_factor") {
|
||||
return BluebirdPromise.reject(new Error("This domain is not protected with single factor. " +
|
||||
"You cannot log in with basic authentication."));
|
||||
}
|
||||
|
||||
const base64Re = new RegExp("^Basic ((?:[A-Za-z0-9+/]{4})*" +
|
||||
"(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?)$");
|
||||
const isTokenValidBase64 = base64Re.test(authorizationHeader);
|
||||
|
@ -52,7 +40,8 @@ export default function (req: Express.Request, res: Express.Response,
|
|||
return vars.usersDatabase.checkUserPassword(username, password);
|
||||
})
|
||||
.then(function (groupsAndEmails) {
|
||||
return AccessControl(req, vars, domain, originalUri, username, groupsAndEmails.groups)
|
||||
return AccessControl(req, vars, urlDecomposition.domain, urlDecomposition.path,
|
||||
username, groupsAndEmails.groups, Level.ONE_FACTOR)
|
||||
.then(() => BluebirdPromise.resolve({
|
||||
username: username,
|
||||
groups: groupsAndEmails.groups
|
||||
|
|
|
@ -5,19 +5,14 @@ import ObjectPath = require("object-path");
|
|||
|
||||
import Exceptions = require("../../Exceptions");
|
||||
import { Configuration } from "../../configuration/schema/Configuration";
|
||||
import Constants = require("../../../../../shared/constants");
|
||||
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
|
||||
import { ServerVariables } from "../../ServerVariables";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||
import { AuthenticationSession }
|
||||
from "../../../../types/AuthenticationSession";
|
||||
import { AuthenticationSessionHandler }
|
||||
from "../../AuthenticationSessionHandler";
|
||||
import AccessControl from "./access_control";
|
||||
|
||||
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
|
||||
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
|
||||
import { URLDecomposer } from "../../utils/URLDecomposer";
|
||||
|
||||
function verify_inactivity(req: Express.Request,
|
||||
authSession: AuthenticationSession,
|
||||
|
@ -48,52 +43,33 @@ function verify_inactivity(req: Express.Request,
|
|||
export default function (req: Express.Request, res: Express.Response,
|
||||
vars: ServerVariables, authSession: AuthenticationSession)
|
||||
: BluebirdPromise<{ username: string, groups: string[] }> {
|
||||
let username: string;
|
||||
let groups: string[];
|
||||
let domain: string;
|
||||
let originalUri: string;
|
||||
|
||||
return new BluebirdPromise(function (resolve, reject) {
|
||||
username = authSession.userid;
|
||||
groups = authSession.groups;
|
||||
return BluebirdPromise.resolve()
|
||||
.then(() => {
|
||||
const username = authSession.userid;
|
||||
const groups = authSession.groups;
|
||||
|
||||
if (!authSession.userid) {
|
||||
reject(new Exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
|
||||
"userid is missing")));
|
||||
return;
|
||||
return BluebirdPromise.reject(new Exceptions.AccessDeniedError(
|
||||
"userid is missing"));
|
||||
}
|
||||
|
||||
const originalUrl = ObjectPath.get<Express.Request, string>(req, "headers.x-original-url");
|
||||
originalUri =
|
||||
const originalUrl = ObjectPath.get<Express.Request, string>(
|
||||
req, "headers.x-original-url");
|
||||
const originalUri =
|
||||
ObjectPath.get<Express.Request, string>(req, "headers.x-original-uri");
|
||||
|
||||
domain = DomainExtractor.fromUrl(originalUrl);
|
||||
const authenticationMethod =
|
||||
MethodCalculator.compute(vars.config.authentication_methods, domain);
|
||||
vars.logger.debug(req, "domain=%s, request_uri=%s, user=%s, groups=%s", domain,
|
||||
originalUri, username, groups.join(","));
|
||||
|
||||
if (!authSession.first_factor)
|
||||
return reject(new Exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
|
||||
"first factor is false")));
|
||||
|
||||
if (authenticationMethod == "two_factor" && !authSession.second_factor)
|
||||
return reject(new Exceptions.AccessDeniedError(
|
||||
Util.format("%s: %s.", SECOND_FACTOR_NOT_VALIDATED_MESSAGE,
|
||||
"second factor is false")));
|
||||
|
||||
resolve();
|
||||
const d = URLDecomposer.fromUrl(originalUrl);
|
||||
vars.logger.debug(req, "domain=%s, path=%s, user=%s, groups=%s", d.domain,
|
||||
d.path, username, groups.join(","));
|
||||
return AccessControl(req, vars, d.domain, d.path, username, groups,
|
||||
authSession.authentication_level);
|
||||
})
|
||||
.then(function () {
|
||||
return AccessControl(req, vars, domain, originalUri, username, groups);
|
||||
})
|
||||
.then(function () {
|
||||
.then(() => {
|
||||
return verify_inactivity(req, authSession,
|
||||
vars.config, vars.logger);
|
||||
})
|
||||
.then(function () {
|
||||
.then(() => {
|
||||
return BluebirdPromise.resolve({
|
||||
username: authSession.userid,
|
||||
groups: authSession.groups
|
||||
|
|
|
@ -10,7 +10,7 @@ describe("storage/mongo/MongoCollection", function () {
|
|||
let mongoClientStub: MongoClientStub;
|
||||
let findStub: Sinon.SinonStub;
|
||||
let findOneStub: Sinon.SinonStub;
|
||||
let insertStub: Sinon.SinonStub;
|
||||
let insertOneStub: Sinon.SinonStub;
|
||||
let updateStub: Sinon.SinonStub;
|
||||
let removeStub: Sinon.SinonStub;
|
||||
let countStub: Sinon.SinonStub;
|
||||
|
@ -21,7 +21,7 @@ describe("storage/mongo/MongoCollection", function () {
|
|||
mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any);
|
||||
findStub = mongoCollectionStub.find as Sinon.SinonStub;
|
||||
findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub;
|
||||
insertStub = mongoCollectionStub.insert as Sinon.SinonStub;
|
||||
insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub;
|
||||
updateStub = mongoCollectionStub.update as Sinon.SinonStub;
|
||||
removeStub = mongoCollectionStub.remove as Sinon.SinonStub;
|
||||
countStub = mongoCollectionStub.count as Sinon.SinonStub;
|
||||
|
@ -63,11 +63,11 @@ describe("storage/mongo/MongoCollection", function () {
|
|||
describe("insert", function () {
|
||||
it("should insert a document in the collection", function () {
|
||||
const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub);
|
||||
insertStub.returns(BluebirdPromise.resolve({}));
|
||||
insertOneStub.returns(BluebirdPromise.resolve({}));
|
||||
|
||||
return collection.insert({ key: "KEY" })
|
||||
.then(function () {
|
||||
Assert(insertStub.calledWith({ key: "KEY" }));
|
||||
Assert(insertOneStub.calledWith({ key: "KEY" }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -40,7 +40,7 @@ export class MongoCollection implements ICollection {
|
|||
|
||||
insert(document: any): Bluebird<any> {
|
||||
return this.collection()
|
||||
.then((collection) => collection.insert(document));
|
||||
.then((collection) => collection.insertOne(document));
|
||||
}
|
||||
|
||||
count(query: any): Bluebird<any> {
|
||||
|
|
46
server/src/lib/utils/URLDecomposer.spec.ts
Normal file
46
server/src/lib/utils/URLDecomposer.spec.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { URLDecomposer } from "./URLDecomposer";
|
||||
import Assert = require("assert");
|
||||
|
||||
describe("utils/URLDecomposer", function () {
|
||||
describe("test fromUrl", function () {
|
||||
it("should return domain from https url", function () {
|
||||
const d = URLDecomposer.fromUrl("https://www.example.com/test/abc");
|
||||
Assert.equal(d.domain, "www.example.com");
|
||||
Assert.equal(d.path, "/test/abc");
|
||||
});
|
||||
|
||||
it("should return domain from http url", function () {
|
||||
const d = URLDecomposer.fromUrl("http://www.example.com/test/abc");
|
||||
Assert.equal(d.domain, "www.example.com");
|
||||
Assert.equal(d.path, "/test/abc");
|
||||
});
|
||||
|
||||
it("should return domain when url contains port", function () {
|
||||
const d = URLDecomposer.fromUrl("https://www.example.com:8080/test/abc");
|
||||
Assert.equal(d.domain, "www.example.com");
|
||||
Assert.equal(d.path, "/test/abc");
|
||||
});
|
||||
|
||||
it("should return default path when no path provided", function () {
|
||||
const d = URLDecomposer.fromUrl("https://www.example.com:8080");
|
||||
Assert.equal(d.domain, "www.example.com");
|
||||
Assert.equal(d.path, "/");
|
||||
});
|
||||
|
||||
it("should return default path when provided", function () {
|
||||
const d = URLDecomposer.fromUrl("https://www.example.com:8080/");
|
||||
Assert.equal(d.domain, "www.example.com");
|
||||
Assert.equal(d.path, "/");
|
||||
});
|
||||
|
||||
it("should return undefined when does not match", function () {
|
||||
const d = URLDecomposer.fromUrl("https:///abc/test");
|
||||
Assert.equal(d, undefined);
|
||||
});
|
||||
|
||||
it("should return undefined when does not match", function () {
|
||||
const d = URLDecomposer.fromUrl("https:///abc/test");
|
||||
Assert.equal(d, undefined);
|
||||
});
|
||||
});
|
||||
});
|
15
server/src/lib/utils/URLDecomposer.ts
Normal file
15
server/src/lib/utils/URLDecomposer.ts
Normal file
|
@ -0,0 +1,15 @@
|
|||
export class URLDecomposer {
|
||||
static fromUrl(url: string): {domain: string, path: string} {
|
||||
if (!url) return;
|
||||
const match = url.match(/https?:\/\/([a-z0-9_.-]+)(:[0-9]+)?(.*)/);
|
||||
|
||||
if (!match) return;
|
||||
|
||||
if (match[1] && !match[3]) {
|
||||
return {domain: match[1], path: "/"};
|
||||
} else if (match[1] && match[3]) {
|
||||
return {domain: match[1], path: match[3]};
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
|
@ -32,23 +32,16 @@ import LoggedIn = require("../routes/loggedin/get");
|
|||
import { ServerVariables } from "../ServerVariables";
|
||||
import Endpoints = require("../../../../shared/api");
|
||||
import { RequireValidatedFirstFactor } from "./middlewares/RequireValidatedFirstFactor";
|
||||
import { RequireTwoFactorEnabled } from "./middlewares/RequireTwoFactorEnabled";
|
||||
|
||||
function setupTotp(app: Express.Application, vars: ServerVariables) {
|
||||
app.post(Endpoints.SECOND_FACTOR_TOTP_POST,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||
TOTPSignGet.default(vars));
|
||||
|
||||
app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_START_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger));
|
||||
|
||||
app.get(Endpoints.SECOND_FACTOR_TOTP_IDENTITY_FINISH_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger));
|
||||
|
||||
IdentityCheckMiddleware.register(app,
|
||||
|
@ -61,37 +54,25 @@ function setupTotp(app: Express.Application, vars: ServerVariables) {
|
|||
|
||||
function setupU2f(app: Express.Application, vars: ServerVariables) {
|
||||
app.get(Endpoints.SECOND_FACTOR_U2F_SIGN_REQUEST_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||
U2FSignRequestGet.default(vars));
|
||||
|
||||
app.post(Endpoints.SECOND_FACTOR_U2F_SIGN_POST,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||
U2FSignPost.default(vars));
|
||||
|
||||
app.get(Endpoints.SECOND_FACTOR_U2F_REGISTER_REQUEST_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||
U2FRegisterRequestGet.default(vars));
|
||||
|
||||
app.post(Endpoints.SECOND_FACTOR_U2F_REGISTER_POST,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||
U2FRegisterPost.default(vars));
|
||||
|
||||
app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_START_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger));
|
||||
|
||||
app.get(Endpoints.SECOND_FACTOR_U2F_IDENTITY_FINISH_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger));
|
||||
|
||||
IdentityCheckMiddleware.register(app,
|
||||
|
@ -124,8 +105,6 @@ export class RestApi {
|
|||
app.get(Endpoints.FIRST_FACTOR_GET, FirstFactorGet.default(vars));
|
||||
|
||||
app.get(Endpoints.SECOND_FACTOR_GET,
|
||||
RequireTwoFactorEnabled.middleware(vars.logger,
|
||||
vars.config.authentication_methods),
|
||||
RequireValidatedFirstFactor.middleware(vars.logger),
|
||||
SecondFactorGet.default(vars));
|
||||
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
import Express = require("express");
|
||||
import BluebirdPromise = require("bluebird");
|
||||
import ErrorReplies = require("../../ErrorReplies");
|
||||
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||
import { MethodCalculator } from "../../authentication/MethodCalculator";
|
||||
import { AuthenticationMethodsConfiguration } from
|
||||
"../../configuration/schema/AuthenticationMethodsConfiguration";
|
||||
|
||||
export class RequireTwoFactorEnabled {
|
||||
static middleware(logger: IRequestLogger,
|
||||
configuration: AuthenticationMethodsConfiguration) {
|
||||
|
||||
return function (req: Express.Request, res: Express.Response,
|
||||
next: Express.NextFunction): void {
|
||||
|
||||
const isSingleFactorMode = MethodCalculator.isSingleFactorOnlyMode(
|
||||
configuration);
|
||||
|
||||
if (isSingleFactorMode) {
|
||||
ErrorReplies.replyWithError401(req, res, logger)(new Error(
|
||||
"Restricted access because server is in single factor mode."));
|
||||
return;
|
||||
}
|
||||
next();
|
||||
};
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import ErrorReplies = require("../../ErrorReplies");
|
|||
import { IRequestLogger } from "../../logging/IRequestLogger";
|
||||
import { AuthenticationSessionHandler } from "../../AuthenticationSessionHandler";
|
||||
import Exceptions = require("../../Exceptions");
|
||||
import { Level } from "../../authentication/Level";
|
||||
|
||||
export class RequireValidatedFirstFactor {
|
||||
static middleware(logger: IRequestLogger) {
|
||||
|
@ -12,7 +13,7 @@ export class RequireValidatedFirstFactor {
|
|||
|
||||
return new BluebirdPromise<void>(function (resolve, reject) {
|
||||
const authSession = AuthenticationSessionHandler.get(req, logger);
|
||||
if (!authSession.userid || !authSession.first_factor)
|
||||
if (!authSession.userid || authSession.authentication_level < Level.ONE_FACTOR)
|
||||
return reject(
|
||||
new Exceptions.FirstFactorValidationError(
|
||||
"First factor has not been validated yet."));
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import U2f = require("u2f");
|
||||
import { Level } from "../src/lib/authentication/Level";
|
||||
|
||||
export interface AuthenticationSession {
|
||||
userid: string;
|
||||
first_factor: boolean;
|
||||
second_factor: boolean;
|
||||
authentication_level: Level;
|
||||
keep_me_logged_in: boolean;
|
||||
last_activity_datetime: number;
|
||||
identity_check?: {
|
||||
|
|
|
@ -33,7 +33,7 @@ Feature: User has access restricted access to domains
|
|||
And I have access to "https://dev.example.com:8080/users/bob/secret.html"
|
||||
And I have no access to "https://admin.example.com:8080/secret.html"
|
||||
And I have access to "https://mx1.mail.example.com:8080/secret.html"
|
||||
And I have no access to "https://single_factor.example.com:8080/secret.html"
|
||||
And I have access to "https://single_factor.example.com:8080/secret.html"
|
||||
And I have access to "https://mx2.mail.example.com:8080/secret.html"
|
||||
|
||||
@need-registered-user-harry
|
||||
|
@ -51,5 +51,5 @@ Feature: User has access restricted access to domains
|
|||
And I have no access to "https://dev.example.com:8080/users/bob/secret.html"
|
||||
And I have no access to "https://admin.example.com:8080/secret.html"
|
||||
And I have no access to "https://mx1.mail.example.com:8080/secret.html"
|
||||
And I have no access to "https://single_factor.example.com:8080/secret.html"
|
||||
And I have access to "https://single_factor.example.com:8080/secret.html"
|
||||
And I have no access to "https://mx2.mail.example.com:8080/secret.html"
|
||||
|
|
|
@ -14,24 +14,3 @@ Feature: Non authenticated users have no access to certain pages
|
|||
| https://login.example.com:8080/api/u2f/sign | 401 | POST |
|
||||
| https://login.example.com:8080/api/u2f/register_request | 401 | GET |
|
||||
| https://login.example.com:8080/api/u2f/register | 401 | POST |
|
||||
|
||||
|
||||
@needs-single_factor-config
|
||||
@need-registered-user-john
|
||||
Scenario: User does not have acces to second factor related endpoints when in single factor mode
|
||||
Given I post "https://login.example.com:8080/api/firstfactor" with body:
|
||||
| key | value |
|
||||
| username | john |
|
||||
| password | password |
|
||||
Then I get the following status code when requesting:
|
||||
| url | code | method |
|
||||
| https://login.example.com:8080/secondfactor | 401 | GET |
|
||||
| https://login.example.com:8080/secondfactor/u2f/identity/start | 401 | GET |
|
||||
| https://login.example.com:8080/secondfactor/u2f/identity/finish | 401 | GET |
|
||||
| https://login.example.com:8080/secondfactor/totp/identity/start | 401 | GET |
|
||||
| https://login.example.com:8080/secondfactor/totp/identity/finish | 401 | GET |
|
||||
| https://login.example.com:8080/api/totp | 401 | POST |
|
||||
| https://login.example.com:8080/api/u2f/sign_request | 401 | GET |
|
||||
| https://login.example.com:8080/api/u2f/sign | 401 | POST |
|
||||
| https://login.example.com:8080/api/u2f/register_request | 401 | GET |
|
||||
| https://login.example.com:8080/api/u2f/register | 401 | POST |
|
|
@ -13,3 +13,4 @@ Feature: User can access certain subdomains with single factor
|
|||
Scenario: User can login using basic authentication
|
||||
When I request "https://single_factor.example.com:8080/secret.html" with username "john" and password "password" using basic authentication
|
||||
Then I receive the secret page
|
||||
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
@needs-single_factor-config
|
||||
Feature: Server is configured as a single factor only server
|
||||
|
||||
@need-registered-user-john
|
||||
Scenario: User is redirected to service after first factor if allowed
|
||||
When I visit "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html"
|
||||
And I login with user "john" and password "password"
|
||||
Then I'm redirected to "https://public.example.com:8080/secret.html"
|
||||
|
||||
@need-registered-user-john
|
||||
Scenario: User is correctly redirected according to default redirection URL
|
||||
When I visit "https://login.example.com:8080"
|
||||
And I login with user "john" and password "password"
|
||||
Then I'm redirected to "https://login.example.com:8080/loggedin"
|
||||
And I sleep for 5 seconds
|
||||
Then I'm redirected to "https://home.example.com:8080/"
|
|
@ -12,11 +12,6 @@ Then("I get an error {int}", function (code: number) {
|
|||
return this.getErrorPage(code);
|
||||
});
|
||||
|
||||
When("I request {string} with method {string}",
|
||||
function (url: string, method: string) {
|
||||
const that = this;
|
||||
});
|
||||
|
||||
function requestAndExpectStatusCode(ctx: any, url: string, method: string,
|
||||
expectedStatusCode: number) {
|
||||
return Request(url, {
|
||||
|
@ -33,7 +28,6 @@ function requestAndExpectStatusCode(ctx: any, url: string, method: string,
|
|||
Assert.equal(statusCode, expectedStatusCode);
|
||||
}
|
||||
catch (e) {
|
||||
console.log(url);
|
||||
console.log("%s (actual) != %s (expected)", statusCode,
|
||||
expectedStatusCode);
|
||||
throw e;
|
||||
|
|
Loading…
Reference in New Issue
Block a user