From 9fc55543fd0205f7a6dba8bbcef17713e3bda5b7 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Mon, 22 Oct 2018 23:21:17 +0200 Subject: [PATCH] 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. --- config.minimal.yml | 24 +- config.template.yml | 18 +- .../src/lib/AuthenticationSessionHandler.ts | 4 +- server/src/lib/Exceptions.ts | 14 +- server/src/lib/FirstFactorValidator.ts | 3 +- server/src/lib/Server.ts | 1 - server/src/lib/ServerVariables.ts | 4 +- server/src/lib/ServerVariablesInitializer.ts | 12 +- .../lib/ServerVariablesMockBuilder.spec.ts | 11 +- .../access_control/AccessController.spec.ts | 367 ----------------- .../AccessControllerStub.spec.ts | 14 - .../lib/access_control/IAccessController.ts | 4 - server/src/lib/authentication/Level.ts | 5 + .../authentication/MethodCalculator.spec.ts | 73 ---- .../lib/authentication/MethodCalculator.ts | 40 -- .../src/lib/authorization/Authorizer.spec.ts | 368 ++++++++++++++++++ .../Authorizer.ts} | 61 +-- .../lib/authorization/AuthorizerStub.spec.ts | 15 + server/src/lib/authorization/IAuthorizer.ts | 5 + server/src/lib/authorization/Level.ts | 6 + .../MultipleDomainMatcher.ts | 0 .../configuration/ConfigurationParser.spec.ts | 10 +- .../SessionConfigurationBuilder.spec.ts | 4 - .../schema/AclConfiguration.spec.ts | 2 +- .../configuration/schema/AclConfiguration.ts | 4 +- ...AuthenticationMethodsConfiguration.spec.ts | 12 - .../AuthenticationMethodsConfiguration.ts | 21 - .../lib/configuration/schema/Configuration.ts | 22 +- server/src/lib/routes/firstfactor/get.ts | 16 +- .../src/lib/routes/firstfactor/post.spec.ts | 3 +- server/src/lib/routes/firstfactor/post.ts | 33 +- .../routes/password-reset/form/post.spec.ts | 7 +- .../src/lib/routes/secondfactor/get.spec.ts | 22 +- server/src/lib/routes/secondfactor/get.ts | 9 - .../secondfactor/totp/sign/post.spec.ts | 8 +- .../lib/routes/secondfactor/totp/sign/post.ts | 3 +- .../routes/secondfactor/u2f/sign/post.spec.ts | 6 +- .../lib/routes/secondfactor/u2f/sign/post.ts | 3 +- .../src/lib/routes/verify/access_control.ts | 45 ++- server/src/lib/routes/verify/get.spec.ts | 111 ++---- server/src/lib/routes/verify/get.ts | 8 +- .../src/lib/routes/verify/get_basic_auth.ts | 24 +- .../lib/routes/verify/get_session_cookie.ts | 48 +-- server/src/lib/utils/URLDecomposer.spec.ts | 46 +++ server/src/lib/utils/URLDecomposer.ts | 15 + server/src/lib/web_server/RestApi.ts | 21 - .../middlewares/RequireTwoFactorEnabled.ts | 27 -- .../RequireValidatedFirstFactor.ts | 3 +- server/types/AuthenticationSession.ts | 4 +- 49 files changed, 677 insertions(+), 909 deletions(-) delete mode 100644 server/src/lib/access_control/AccessController.spec.ts delete mode 100644 server/src/lib/access_control/AccessControllerStub.spec.ts delete mode 100644 server/src/lib/access_control/IAccessController.ts create mode 100644 server/src/lib/authentication/Level.ts delete mode 100644 server/src/lib/authentication/MethodCalculator.spec.ts delete mode 100644 server/src/lib/authentication/MethodCalculator.ts create mode 100644 server/src/lib/authorization/Authorizer.spec.ts rename server/src/lib/{access_control/AccessController.ts => authorization/Authorizer.ts} (63%) create mode 100644 server/src/lib/authorization/AuthorizerStub.spec.ts create mode 100644 server/src/lib/authorization/IAuthorizer.ts create mode 100644 server/src/lib/authorization/Level.ts rename server/src/lib/{access_control => authorization}/MultipleDomainMatcher.ts (100%) delete mode 100644 server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.spec.ts delete mode 100644 server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.ts create mode 100644 server/src/lib/utils/URLDecomposer.spec.ts create mode 100644 server/src/lib/utils/URLDecomposer.ts delete mode 100644 server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts diff --git a/config.minimal.yml b/config.minimal.yml index 9a25844dc..9170fcb92 100644 --- a/config.minimal.yml +++ b/config.minimal.yml @@ -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/.*$' diff --git a/config.template.yml b/config.template.yml index abbca0e30..5b74c247a 100644 --- a/config.template.yml +++ b/config.template.yml @@ -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/.*$' diff --git a/server/src/lib/AuthenticationSessionHandler.ts b/server/src/lib/AuthenticationSessionHandler.ts index f7ed752f6..57361bf8a 100644 --- a/server/src/lib/AuthenticationSessionHandler.ts +++ b/server/src/lib/AuthenticationSessionHandler.ts @@ -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, diff --git a/server/src/lib/Exceptions.ts b/server/src/lib/Exceptions.ts index e8c797d0a..83fa4eb6b 100644 --- a/server/src/lib/Exceptions.ts +++ b/server/src/lib/Exceptions.ts @@ -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"; - (Object).setPrototypeOf(this, DomainAccessDenied.prototype); + this.name = "NotAuthenticatedError"; + (Object).setPrototypeOf(this, NotAuthenticatedError.prototype); + } +} + +export class NotAuthorizedError extends Error { + constructor(message?: string) { + super(message); + this.name = "NotAuthanticatedError"; + (Object).setPrototypeOf(this, NotAuthorizedError.prototype); } } diff --git a/server/src/lib/FirstFactorValidator.ts b/server/src/lib/FirstFactorValidator.ts index 36de5ae5d..231060002 100644 --- a/server/src/lib/FirstFactorValidator.ts +++ b/server/src/lib/FirstFactorValidator.ts @@ -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 { 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.")); diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index ada66f09e..4090f6294 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -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"; diff --git a/server/src/lib/ServerVariables.ts b/server/src/lib/ServerVariables.ts index 6b3b89e20..cd3dd6dc8 100644 --- a/server/src/lib/ServerVariables.ts +++ b/server/src/lib/ServerVariables.ts @@ -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; } \ No newline at end of file diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts index 7069ef1c8..df79238cc 100644 --- a/server/src/lib/ServerVariablesInitializer.ts +++ b/server/src/lib/ServerVariablesInitializer.ts @@ -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 { @@ -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, diff --git a/server/src/lib/ServerVariablesMockBuilder.spec.ts b/server/src/lib/ServerVariablesMockBuilder.spec.ts index 25014e218..7874702a0 100644 --- a/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ b/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -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, diff --git a/server/src/lib/access_control/AccessController.spec.ts b/server/src/lib/access_control/AccessController.spec.ts deleted file mode 100644 index 057e23d8e..000000000 --- a/server/src/lib/access_control/AccessController.spec.ts +++ /dev/null @@ -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"])); - }); - }); - }); -}); diff --git a/server/src/lib/access_control/AccessControllerStub.spec.ts b/server/src/lib/access_control/AccessControllerStub.spec.ts deleted file mode 100644 index 607454690..000000000 --- a/server/src/lib/access_control/AccessControllerStub.spec.ts +++ /dev/null @@ -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); - } -} diff --git a/server/src/lib/access_control/IAccessController.ts b/server/src/lib/access_control/IAccessController.ts deleted file mode 100644 index 83681b89a..000000000 --- a/server/src/lib/access_control/IAccessController.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export interface IAccessController { - isAccessAllowed(domain: string, resource: string, user: string, groups: string[]): boolean; -} \ No newline at end of file diff --git a/server/src/lib/authentication/Level.ts b/server/src/lib/authentication/Level.ts new file mode 100644 index 000000000..57b6a2346 --- /dev/null +++ b/server/src/lib/authentication/Level.ts @@ -0,0 +1,5 @@ +export enum Level { + NOT_AUTHENTICATED = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2 +} \ No newline at end of file diff --git a/server/src/lib/authentication/MethodCalculator.spec.ts b/server/src/lib/authentication/MethodCalculator.spec.ts deleted file mode 100644 index 6c6c916a0..000000000 --- a/server/src/lib/authentication/MethodCalculator.spec.ts +++ /dev/null @@ -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)); - }); - }); -}); \ No newline at end of file diff --git a/server/src/lib/authentication/MethodCalculator.ts b/server/src/lib/authentication/MethodCalculator.ts deleted file mode 100644 index 961a8402d..000000000 --- a/server/src/lib/authentication/MethodCalculator.ts +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/server/src/lib/authorization/Authorizer.spec.ts b/server/src/lib/authorization/Authorizer.spec.ts new file mode 100644 index 000000000..814773049 --- /dev/null +++ b/server/src/lib/authorization/Authorizer.spec.ts @@ -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); + }); + }); + }); +}); diff --git a/server/src/lib/access_control/AccessController.ts b/server/src/lib/authorization/Authorizer.ts similarity index 63% rename from server/src/lib/access_control/AccessController.ts rename to server/src/lib/authorization/Authorizer.ts index dd7328fdd..e235a3914 100644 --- a/server/src/lib/access_control/AccessController.ts +++ b/server/src/lib/authorization/Authorizer.ts @@ -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; } } \ No newline at end of file diff --git a/server/src/lib/authorization/AuthorizerStub.spec.ts b/server/src/lib/authorization/AuthorizerStub.spec.ts new file mode 100644 index 000000000..3b8ece28e --- /dev/null +++ b/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -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); + } +} diff --git a/server/src/lib/authorization/IAuthorizer.ts b/server/src/lib/authorization/IAuthorizer.ts new file mode 100644 index 000000000..1b5caabc8 --- /dev/null +++ b/server/src/lib/authorization/IAuthorizer.ts @@ -0,0 +1,5 @@ +import { Level } from "./Level"; + +export interface IAuthorizer { + authorization(domain: string, resource: string, user: string, groups: string[]): Level; +} \ No newline at end of file diff --git a/server/src/lib/authorization/Level.ts b/server/src/lib/authorization/Level.ts new file mode 100644 index 000000000..d12802610 --- /dev/null +++ b/server/src/lib/authorization/Level.ts @@ -0,0 +1,6 @@ +export enum Level { + BYPASS = 0, + ONE_FACTOR = 1, + TWO_FACTOR = 2, + DENY = 3 +} \ No newline at end of file diff --git a/server/src/lib/access_control/MultipleDomainMatcher.ts b/server/src/lib/authorization/MultipleDomainMatcher.ts similarity index 100% rename from server/src/lib/access_control/MultipleDomainMatcher.ts rename to server/src/lib/authorization/MultipleDomainMatcher.ts diff --git a/server/src/lib/configuration/ConfigurationParser.spec.ts b/server/src/lib/configuration/ConfigurationParser.spec.ts index ba16e1640..2baefc8a9 100644 --- a/server/src/lib/configuration/ConfigurationParser.spec.ts +++ b/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -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: {} diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts index 1ff48ea4e..0a4c02c7c 100644 --- a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -54,10 +54,6 @@ describe("configuration/SessionConfigurationBuilder", function () { local: { in_memory: true } - }, - authentication_methods: { - default_method: "two_factor", - per_subdomain_methods: {} } }; diff --git a/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/server/src/lib/configuration/schema/AclConfiguration.spec.ts index 8c5ef3444..6b2f47f9e 100644 --- a/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ b/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -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, {}); diff --git a/server/src/lib/configuration/schema/AclConfiguration.ts b/server/src/lib/configuration/schema/AclConfiguration.ts index bba3c4dca..e29dceb23 100644 --- a/server/src/lib/configuration/schema/AclConfiguration.ts +++ b/server/src/lib/configuration/schema/AclConfiguration.ts @@ -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) { diff --git a/server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.spec.ts b/server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.spec.ts deleted file mode 100644 index f39ae671b..000000000 --- a/server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.spec.ts +++ /dev/null @@ -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, []); - }); -}); \ No newline at end of file diff --git a/server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.ts b/server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.ts deleted file mode 100644 index 1b454d078..000000000 --- a/server/src/lib/configuration/schema/AuthenticationMethodsConfiguration.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/server/src/lib/configuration/schema/Configuration.ts b/server/src/lib/configuration/schema/Configuration.ts index 7777125e4..9798bc83e 100644 --- a/server/src/lib/configuration/schema/Configuration.ts +++ b/server/src/lib/configuration/schema/Configuration.ts @@ -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; diff --git a/server/src/lib/routes/firstfactor/get.ts b/server/src/lib/routes/firstfactor/get.ts index dc7260fb9..d94f656c8 100644 --- a/server/src/lib/routes/firstfactor/get.ts +++ b/server/src/lib/routes/firstfactor/get.ts @@ -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 { 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(); }); }; diff --git a/server/src/lib/routes/firstfactor/post.spec.ts b/server/src/lib/routes/firstfactor/post.spec.ts index 98479d639..e1d078cdd 100644 --- a/server/src/lib/routes/firstfactor/post.spec.ts +++ b/server/src/lib/routes/firstfactor/post.spec.ts @@ -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()); diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index 1698fd771..ba45c3ece 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -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) { diff --git a/server/src/lib/routes/password-reset/form/post.spec.ts b/server/src/lib/routes/password-reset/form/post.spec.ts index 8d6389718..ed029c906 100644 --- a/server/src/lib/routes/password-reset/form/post.spec.ts +++ b/server/src/lib/routes/password-reset/form/post.spec.ts @@ -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(); }); }); diff --git a/server/src/lib/routes/secondfactor/get.spec.ts b/server/src/lib/routes/secondfactor/get.spec.ts index f7cc8cd33..6c77e1f69 100644 --- a/server/src/lib/routes/secondfactor/get.spec.ts +++ b/server/src/lib/routes/secondfactor/get.spec.ts @@ -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 () { diff --git a/server/src/lib/routes/secondfactor/get.ts b/server/src/lib/routes/secondfactor/get.ts index 71e495f37..9f6deb4c6 100644 --- a/server/src/lib/routes/secondfactor/get.ts +++ b/server/src/lib/routes/secondfactor/get.ts @@ -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 { 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, diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts b/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts index 651f7d77f..70a20d39d 100644 --- a/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts +++ b/server/src/lib/routes/secondfactor/totp/sign/post.spec.ts @@ -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." diff --git a/server/src/lib/routes/secondfactor/totp/sign/post.ts b/server/src/lib/routes/secondfactor/totp/sign/post.ts index 1d62b5497..34a276d12 100644 --- a/server/src/lib/routes/secondfactor/totp/sign/post.ts +++ b/server/src/lib/routes/secondfactor/totp/sign/post.ts @@ -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(); }) diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts index 034a73ebd..9b137e66d 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign/post.spec.ts @@ -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); }); }); diff --git a/server/src/lib/routes/secondfactor/u2f/sign/post.ts b/server/src/lib/routes/secondfactor/u2f/sign/post.ts index 8e8436ef3..7ee711c2c 100644 --- a/server/src/lib/routes/secondfactor/u2f/sign/post.ts +++ b/server/src/lib/routes/secondfactor/u2f/sign/post.ts @@ -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 { @@ -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(); }) diff --git a/server/src/lib/routes/verify/access_control.ts b/server/src/lib/routes/verify/access_control.ts index 99a7f807d..86e740296 100644 --- a/server/src/lib/routes/verify/access_control.ts +++ b/server/src/lib/routes/verify/access_control.ts @@ -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(); diff --git a/server/src/lib/routes/verify/get.spec.ts b/server/src/lib/routes/verify/get.spec.ts index 3643dc025..376fa622c 100644 --- a/server/src/lib/routes/verify/get.spec.ts +++ b/server/src/lib/routes/verify/get.spec.ts @@ -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"], }); diff --git a/server/src/lib/routes/verify/get.ts b/server/src/lib/routes/verify/get.ts index 08d094373..f73861696 100644 --- a/server/src/lib/routes/verify/get.ts +++ b/server/src/lib/routes/verify/get.ts @@ -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); diff --git a/server/src/lib/routes/verify/get_basic_auth.ts b/server/src/lib/routes/verify/get_basic_auth.ts index 0710d88bf..c57a01252 100644 --- a/server/src/lib/routes/verify/get_basic_auth.ts +++ b/server/src/lib/routes/verify/get_basic_auth.ts @@ -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(req, "headers.x-original-url"); + const urlDecomposition = URLDecomposer.fromUrl(uri); return BluebirdPromise.resolve() .then(() => { - const originalUrl = ObjectPath.get(req, "headers.x-original-url"); - domain = DomainExtractor.fromUrl(originalUrl); - originalUri = - ObjectPath.get(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 diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts index 476aa8466..dc7453ad8 100644 --- a/server/src/lib/routes/verify/get_session_cookie.ts +++ b/server/src/lib/routes/verify/get_session_cookie.ts @@ -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(req, "headers.x-original-url"); - originalUri = + const originalUri = ObjectPath.get(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 diff --git a/server/src/lib/utils/URLDecomposer.spec.ts b/server/src/lib/utils/URLDecomposer.spec.ts new file mode 100644 index 000000000..cbb038738 --- /dev/null +++ b/server/src/lib/utils/URLDecomposer.spec.ts @@ -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); + }); + }); +}); \ No newline at end of file diff --git a/server/src/lib/utils/URLDecomposer.ts b/server/src/lib/utils/URLDecomposer.ts new file mode 100644 index 000000000..9bdf2e9d2 --- /dev/null +++ b/server/src/lib/utils/URLDecomposer.ts @@ -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; + } +} \ No newline at end of file diff --git a/server/src/lib/web_server/RestApi.ts b/server/src/lib/web_server/RestApi.ts index 56408c750..9144a15b9 100644 --- a/server/src/lib/web_server/RestApi.ts +++ b/server/src/lib/web_server/RestApi.ts @@ -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)); diff --git a/server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts b/server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts deleted file mode 100644 index 6f8db4058..000000000 --- a/server/src/lib/web_server/middlewares/RequireTwoFactorEnabled.ts +++ /dev/null @@ -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(); - }; - } -} \ No newline at end of file diff --git a/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts b/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts index 3a7af1548..ecfd75765 100644 --- a/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts +++ b/server/src/lib/web_server/middlewares/RequireValidatedFirstFactor.ts @@ -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(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.")); diff --git a/server/types/AuthenticationSession.ts b/server/types/AuthenticationSession.ts index e299bc431..bbed0e715 100644 --- a/server/types/AuthenticationSession.ts +++ b/server/types/AuthenticationSession.ts @@ -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?: {