Integrate more policy options in ACL rules.

The possible values for ACL policies are now: bypass, one_factor, two_factor,
deny.

This change also deprecate auth_methods because the method is now associated
directly to a resource in the ACLs instead of a domain.
pull/289/head
Clement Michaud 2018-10-22 23:21:17 +02:00 committed by Clement Michaud
parent d898fa2c0c
commit 9fc55543fd
49 changed files with 677 additions and 909 deletions

View File

@ -22,27 +22,21 @@ 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
any:
- domain: single_factor.example.com
policy: one_factor
groups:
admins:
# All resources in all domains
- domain: '*.example.com'
policy: allow
policy: two_factor
# Except mx2.mail.example.com (it restricts the first rule)
#- domain: 'mx2.mail.example.com'
# policy: deny
@ -51,19 +45,19 @@ access_control:
users:
john:
- domain: dev.example.com
policy: allow
policy: two_factor
resources:
- '^/users/john/.*$'
harry:
- domain: dev.example.com
policy: allow
policy: two_factor
resources:
- '^/users/harry/.*$'
bob:
- domain: '*.mail.example.com'
policy: allow
policy: two_factor
- domain: 'dev.example.com'
policy: allow
policy: two_factor
resources:
- '^/users/bob/.*$'

View File

@ -103,7 +103,7 @@ authentication_backend:
authentication_methods:
default_method: two_factor
per_subdomain_methods:
single_factor.example.com: single_factor
# Access Control
@ -148,7 +148,9 @@ access_control:
# The value is a list of rules.
any:
- domain: public.example.com
policy: allow
policy: two_factor
- domain: single_factor.example.com
policy: one_factor
# Group-based rules. The key is a group name and the value
# is a list of rules.
@ -156,13 +158,13 @@ access_control:
admin:
# All resources in all domains
- domain: '*.example.com'
policy: allow
policy: two_factor
# Except mx2.mail.example.com (it restricts the first rule)
- domain: 'mx2.mail.example.com'
policy: deny
dev:
- domain: dev.example.com
policy: allow
policy: two_factor
resources:
- '^/groups/dev/.*$'
@ -171,19 +173,19 @@ access_control:
users:
john:
- domain: dev.example.com
policy: allow
policy: two_factor
resources:
- '^/users/john/.*$'
harry:
- domain: dev.example.com
policy: allow
policy: two_factor
resources:
- '^/users/harry/.*$'
bob:
- domain: '*.mail.example.com'
policy: allow
policy: two_factor
- domain: 'dev.example.com'
policy: allow
policy: two_factor
resources:
- '^/users/bob/.*$'

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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."));

View File

@ -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";

View File

@ -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;
}

View File

@ -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,

View File

@ -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,

View File

@ -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"]));
});
});
});
});

View File

@ -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);
}
}

View File

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

View File

@ -0,0 +1,5 @@
export enum Level {
NOT_AUTHENTICATED = 0,
ONE_FACTOR = 1,
TWO_FACTOR = 2
}

View File

@ -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));
});
});
});

View File

@ -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);
}
}

View File

@ -0,0 +1,368 @@
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("home.example.com", "/", "user1", ["group1", "group2"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/abc", "user1", ["group1", "group2"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/", "user2", ["group1", "group2"]), Level.BYPASS);
Assert.equal(authorizer.authorization("admin.example.com", "/", "user3", ["group3"]), Level.BYPASS);
});
});
describe("configuration is not null", function () {
beforeEach(function () {
configuration = {
default_policy: "deny",
any: [],
users: {},
groups: {}
};
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("home.example.com", "/", "user1", ["group1"]), Level.DENY);
});
it("should control access when multiple domain matcher is provided", function () {
configuration.users["user1"] = [{
domain: "*.mail.example.com",
policy: "two_factor",
resources: [".*"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("mx1.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("mx1.server.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("mail.example.com", "/", "user1", ["group1"]), Level.DENY);
});
it("should allow access to all resources when resources is not provided", function () {
configuration.users["user1"] = [{
domain: "*.mail.example.com",
policy: "two_factor"
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("mx1.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("mx1.server.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("mail.example.com", "/", "user1", ["group1"]), Level.DENY);
});
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: "two_factor",
resources: [".*"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("another.home.example.com", "/", "user1", ["group1"]), Level.DENY);
});
it("should deny to other users", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: [".*"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user2", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user2", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("another.home.example.com", "/", "user2", ["group1"]), Level.DENY);
});
it("should allow user access only to specific resources", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["/private/.*", "^/begin", "/end$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/class", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/middle/private/class", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/begin", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/not/begin", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/abc/end", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/abc/end/x", "user1", ["group1"]), Level.DENY);
});
it("should allow access to multiple domains", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: [".*"]
}, {
domain: "home1.example.com",
policy: "one_factor",
resources: [".*"]
}, {
domain: "home2.example.com",
policy: "deny",
resources: [".*"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home1.example.com", "/", "user1", ["group1"]), Level.ONE_FACTOR);
Assert.equal(authorizer.authorization("home2.example.com", "/", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home3.example.com", "/", "user1", ["group1"]), Level.DENY);
});
it("should always apply latest rule", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/my/.*"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/my/private/.*"]
}, {
domain: "home.example.com",
policy: "one_factor",
resources: ["/my/private/resource"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/my/poney", "user1", ["group1"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/my/private/duck", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/my/private/resource", "user1", ["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.groups["group1"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/$"]
}];
configuration.groups["group2"] = [{
domain: "home.example.com",
policy: "one_factor",
resources: ["^/test$"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/private$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1",
["group1", "group2", "group3"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/test", "user1",
["group1", "group2", "group3"]), Level.ONE_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user1",
["group1", "group2", "group3"]), Level.DENY);
Assert.equal(authorizer.authorization("another.home.example.com", "/", "user1",
["group1", "group2", "group3"]), Level.DENY);
});
});
});
describe("check any rules", function () {
it("should control access when any rules are defined", function () {
configuration.any = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/public$"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/private$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/public", "user1",
["group1", "group2", "group3"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user1",
["group1", "group2", "group3"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/public", "user4",
["group5"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private", "user4",
["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("home.example.com", "/", "user1", ["group1"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/test", "user1", ["group1"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "user1", ["group1"]), Level.BYPASS);
});
it("should deny access to one resource when defined", function () {
configuration.users["user1"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["/test"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.BYPASS);
Assert.equal(authorizer.authorization("home.example.com", "/test", "user1", ["group1"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "user1", ["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.any = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/public$", "^/$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.groups["admins"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: [".*"]
}];
configuration.groups["admin-private"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/?.*"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/john$"]
}];
configuration.users["harry"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/private/harry"]
}, {
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/b.*$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/public", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/admin", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private/john", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private/harry", "admin", ["admins"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/public", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/admin", "john", ["dev", "admin-private"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private/john", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/private/harry", "john", ["dev", "admin-private"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/", "harry", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/public", "harry", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev", "harry", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/admin", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/josh", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/john", "harry", ["dev"]), Level.DENY);
Assert.equal(authorizer.authorization("home.example.com", "/private/harry", "harry", ["dev"]), Level.TWO_FACTOR);
});
it("should control access when allowed at group level and denied at user level", function () {
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY);
});
it("should control access when allowed at 'any' level and denied at user level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.DENY);
});
it("should control access when allowed at 'any' level and denied at group level", function () {
configuration.any = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["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.any = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
configuration.groups["dev"] = [{
domain: "home.example.com",
policy: "deny",
resources: ["^/dev/bob$"]
}];
configuration.users["john"] = [{
domain: "home.example.com",
policy: "two_factor",
resources: ["^/dev/?.*$"]
}];
Assert.equal(authorizer.authorization("home.example.com", "/dev/john", "john", ["dev"]), Level.TWO_FACTOR);
Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev"]), Level.TWO_FACTOR);
});
});
});
});

View File

@ -1,19 +1,9 @@
import { ACLConfiguration, ACLPolicy, ACLRule } from "../configuration/schema/AclConfiguration";
import { IAccessController } from "./IAccessController";
import { IAuthorizer } from "./IAuthorizer";
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";
}
import { Level } from "./Level";
function MatchDomain(actualDomain: string) {
return function (rule: ACLRule): boolean {
@ -34,11 +24,7 @@ function MatchResource(actualResource: string) {
};
}
function SelectPolicy(rule: ACLRule): ("allow" | "deny") {
return rule.policy;
}
export class AccessController implements IAccessController {
export class Authorizer implements IAuthorizer {
private logger: Winston;
private readonly configuration: ACLConfiguration;
@ -47,23 +33,6 @@ export class AccessController implements IAccessController {
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 [];
@ -88,24 +57,22 @@ export class AccessController implements IAccessController {
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;
authorization(domain: string, resource: string, user: string, groups: string[]): Level {
if (!this.configuration) return Level.BYPASS;
const allRules = this.getMatchingAllRules(domain, resource);
const groupRules = this.getMatchingGroupRules(groups, domain, resource);
const userRules = this.getMatchingUserRules(user, domain, resource);
const rules = allRules.concat(groupRules).concat(userRules).reverse();
const policy = rules.map(r => r.policy).concat([this.configuration.default_policy])[0];
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();
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;
}
}

View File

@ -0,0 +1,15 @@
import Sinon = require("sinon");
import { IAuthorizer } from "./IAuthorizer";
import { Level } from "./Level";
export class AuthorizerStub implements IAuthorizer {
authorizationMock: Sinon.SinonStub;
constructor() {
this.authorizationMock = Sinon.stub();
}
authorization(domain: string, resource: string, user: string, groups: string[]): Level {
return this.authorizationMock(domain, resource, user, groups);
}
}

View File

@ -0,0 +1,5 @@
import { Level } from "./Level";
export interface IAuthorizer {
authorization(domain: string, resource: string, user: string, groups: string[]): Level;
}

View File

@ -0,0 +1,6 @@
export enum Level {
BYPASS = 0,
ONE_FACTOR = 1,
TWO_FACTOR = 2,
DENY = 3
}

View File

@ -127,12 +127,12 @@ describe("configuration/ConfigurationParser", function () {
default_policy: "deny",
any: [{
domain: "public.example.com",
policy: "allow"
policy: "two_factor"
}],
users: {
"user": [{
domain: "www.example.com",
policy: "allow"
policy: "two_factor"
}]
},
groups: {}
@ -142,12 +142,12 @@ describe("configuration/ConfigurationParser", function () {
default_policy: "deny",
any: [{
domain: "public.example.com",
policy: "allow"
policy: "two_factor"
}],
users: {
"user": [{
domain: "www.example.com",
policy: "allow"
policy: "two_factor"
}]
},
groups: {}
@ -160,7 +160,7 @@ describe("configuration/ConfigurationParser", function () {
userConfig.access_control = {} as any;
const config = ConfigurationParser.parse(userConfig);
Assert.deepEqual(config.access_control, {
default_policy: "allow",
default_policy: "bypass",
any: [],
users: {},
groups: {}

View File

@ -54,10 +54,6 @@ describe("configuration/SessionConfigurationBuilder", function () {
local: {
in_memory: true
}
},
authentication_methods: {
default_method: "two_factor",
per_subdomain_methods: {}
}
};

View File

@ -6,7 +6,7 @@ describe("configuration/schema/AclConfiguration", function() {
const configuration: ACLConfiguration = {};
const newConfiguration = complete(configuration);
Assert.deepEqual(newConfiguration.default_policy, "allow");
Assert.deepEqual(newConfiguration.default_policy, "bypass");
Assert.deepEqual(newConfiguration.any, []);
Assert.deepEqual(newConfiguration.groups, {});
Assert.deepEqual(newConfiguration.users, {});

View File

@ -1,5 +1,5 @@
export type ACLPolicy = "deny" | "allow";
export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor";
export type ACLRule = {
domain: string;
@ -23,7 +23,7 @@ export function complete(configuration: ACLConfiguration): ACLConfiguration {
? JSON.parse(JSON.stringify(configuration)) : {};
if (!newConfiguration.default_policy) {
newConfiguration.default_policy = "allow";
newConfiguration.default_policy = "bypass";
}
if (!newConfiguration.any) {

View File

@ -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, []);
});
});

View File

@ -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;
}

View File

@ -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;
@ -41,25 +38,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;

View File

@ -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();
});
};

View File

@ -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());

View File

@ -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,19 @@ 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(
decomposition.domain, decomposition.path, username, groups)
: AuthorizationLevel.TWO_FACTOR;
if (emails.length > 0)
authSession.email = emails[0];
@ -73,8 +69,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 +78,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 +89,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) {

View File

@ -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();
});
});

View File

@ -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 () {

View File

@ -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,

View File

@ -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."

View File

@ -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();
})

View File

@ -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);
});
});

View File

@ -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();
})

View File

@ -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, path: string,
username: 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, path, username, 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 unauthorized to access %s%s", username, domain, path)));
return;
}
reject(new Exceptions.NotAuthenticatedError(Util.format(
"User '%s' is not sufficiently authenticated.", username, domain, path)));
return;
}
resolve();

View File

@ -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.any = [{
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"],
});

View File

@ -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);

View File

@ -4,31 +4,24 @@ import ObjectPath = require("object-path");
import { ServerVariables } from "../../ServerVariables";
import { AuthenticationSession }
from "../../../../types/AuthenticationSession";
<<<<<<< HEAD
import { DomainExtractor } from "../../../../../shared/DomainExtractor";
import { MethodCalculator } from "../../authentication/MethodCalculator";
=======
>>>>>>> Integrate more policy options in ACL rules.
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 +45,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

View File

@ -5,16 +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";
import { URLDecomposer } from "../../utils/URLDecomposer";
const FIRST_FACTOR_NOT_VALIDATED_MESSAGE = "First factor not yet validated";
const SECOND_FACTOR_NOT_VALIDATED_MESSAGE = "Second factor not yet validated";
@ -48,52 +46,32 @@ 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(
return BluebirdPromise.reject(new Exceptions.AccessDeniedError(
Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE,
"userid is missing")));
return;
}
const originalUrl = ObjectPath.get<Express.Request, string>(req, "headers.x-original-url");
originalUri =
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

View 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);
});
});
});

View 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;
}
}

View File

@ -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));

View File

@ -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();
};
}
}

View File

@ -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."));

View File

@ -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?: {