From 9fc55543fd0205f7a6dba8bbcef17713e3bda5b7 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Mon, 22 Oct 2018 23:21:17 +0200 Subject: [PATCH 1/4] 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 9a25844d..9170fcb9 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 abbca0e3..5b74c247 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 f7ed752f..57361bf8 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 e8c797d0..83fa4eb6 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 36de5ae5..23106000 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 ada66f09..4090f629 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 6b3b89e2..cd3dd6dc 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 7069ef1c..df79238c 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 25014e21..7874702a 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 057e23d8..00000000 --- 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 60745469..00000000 --- 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 83681b89..00000000 --- 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 00000000..57b6a234 --- /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 6c6c916a..00000000 --- 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 961a8402..00000000 --- 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 00000000..81477304 --- /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 dd7328fd..e235a391 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 00000000..3b8ece28 --- /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 00000000..1b5caabc --- /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 00000000..d1280261 --- /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 ba16e164..2baefc8a 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 1ff48ea4..0a4c02c7 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 8c5ef344..6b2f47f9 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 bba3c4dc..e29dceb2 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 f39ae671..00000000 --- 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 1b454d07..00000000 --- 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 7777125e..9798bc83 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 dc7260fb..d94f656c 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 98479d63..e1d078cd 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 1698fd77..ba45c3ec 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 8d638971..ed029c90 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 f7cc8cd3..6c77e1f6 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 71e495f3..9f6deb4c 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 651f7d77..70a20d39 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 1d62b549..34a276d1 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 034a73eb..9b137e66 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 8e8436ef..7ee711c2 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 99a7f807..86e74029 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 3643dc02..376fa622 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 08d09437..f7386169 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 0710d88b..c57a0125 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 476aa846..dc7453ad 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 00000000..cbb03873 --- /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 00000000..9bdf2e9d --- /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 56408c75..9144a15b 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 6f8db405..00000000 --- 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 3a7af154..ecfd7576 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 e299bc43..bbed0e71 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?: { From 2bc650fd972f96b70bedec78024bb5d48c7255be Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 24 Oct 2018 00:10:23 +0200 Subject: [PATCH 2/4] Create a docker-compose.dev.yml to reproduce integration test cases. --- docker-compose.dev.yml | 4 +--- docker-compose.minimal.dev.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 docker-compose.minimal.dev.yml diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1fad8eff..44e5350c 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -10,9 +10,7 @@ services: - ./dist/server/src/public_html:/usr/src/server/src/public_html - ./client:/usr/src/client - ./shared:/usr/src/shared - - ./config.minimal.yml:/etc/authelia/config.yml:ro - - /tmp/authelia:/tmp/authelia - - ./users_database.yml:/etc/authelia/users_database.yml + - ./config.template.yml:/etc/authelia/config.yml:ro environment: - NODE_TLS_REJECT_UNAUTHORIZED=0 depends_on: diff --git a/docker-compose.minimal.dev.yml b/docker-compose.minimal.dev.yml new file mode 100644 index 00000000..f3647975 --- /dev/null +++ b/docker-compose.minimal.dev.yml @@ -0,0 +1,27 @@ +version: '2' +services: + authelia: + build: + context: . + dockerfile: Dockerfile.dev + restart: always + volumes: + - ./server:/usr/src/server + - ./dist/server/src/public_html:/usr/src/server/src/public_html + - ./client:/usr/src/client + - ./shared:/usr/src/shared + - ./config.minimal.yml:/etc/authelia/config.yml:ro + - /tmp/authelia:/tmp/authelia + - ./users_database.yml:/etc/authelia/users_database.yml + environment: + - NODE_TLS_REJECT_UNAUTHORIZED=0 + depends_on: + - redis + networks: + - example-network + command: + - "./node_modules/.bin/ts-node" + - "-P" + - "server/tsconfig.json" + - "server/src/index.ts" + - "/etc/authelia/config.yml" From 97bfafb6eb8539cb0f4ef000db26696a03d2b15a Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Wed, 24 Oct 2018 00:29:09 +0200 Subject: [PATCH 3/4] [BREAKING] Flatten the ACL rules to enable some use cases. With previous configuration format rules were not ordered between groups and thus not predictable. Also in some cases `any` must have been a higher precedence than `groups`. Flattening the rules let the user apply whatever policy he can think of. When several rules match the (subject, domain, resource), the first one is applied. NOTE: This commit changed the format for declaring ACLs. Be sure to update your configuration file before upgrading. --- config.minimal.yml | 56 ++--- config.template.yml | 172 +++++++--------- server/src/lib/IdentityCheckMiddleware.ts | 8 +- .../src/lib/authorization/Authorizer.spec.ts | 194 +++++++++--------- server/src/lib/authorization/Authorizer.ts | 65 +++--- .../configuration/ConfigurationParser.spec.ts | 34 ++- .../SessionConfigurationBuilder.spec.ts | 4 +- .../schema/AclConfiguration.spec.ts | 28 ++- .../configuration/schema/AclConfiguration.ts | 35 ++-- .../lib/configuration/schema/Configuration.ts | 10 +- .../schema/NotifierConfiguration.spec.ts | 4 +- server/src/lib/routes/verify/get.spec.ts | 2 +- .../lib/storage/mongo/MongoCollection.spec.ts | 8 +- .../src/lib/storage/mongo/MongoCollection.ts | 2 +- test/features/access-control.feature | 4 +- test/features/restrictions.feature | 21 -- test/features/single-factor-domain.feature | 1 + .../single-factor-only-server.feature | 16 -- .../features/step_definitions/restrictions.ts | 1 - 19 files changed, 317 insertions(+), 348 deletions(-) delete mode 100644 test/features/single-factor-only-server.feature diff --git a/config.minimal.yml b/config.minimal.yml index 9170fcb9..8da7bc5e 100644 --- a/config.minimal.yml +++ b/config.minimal.yml @@ -29,37 +29,37 @@ totp: access_control: # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. default_policy: deny - any: + + rules: - domain: single_factor.example.com policy: one_factor - groups: - admins: - # All resources in all domains - - domain: '*.example.com' - policy: two_factor - # Except mx2.mail.example.com (it restricts the first rule) - #- domain: 'mx2.mail.example.com' - # policy: deny - # User-based rules. - users: - john: - - domain: dev.example.com - policy: two_factor - resources: - - '^/users/john/.*$' - harry: - - domain: dev.example.com - policy: two_factor - resources: - - '^/users/harry/.*$' - bob: - - domain: '*.mail.example.com' - policy: two_factor - - domain: 'dev.example.com' - policy: two_factor - resources: - - '^/users/bob/.*$' + - domain: '*.example.com' + subject: "group:admins" + policy: two_factor + + - domain: dev.example.com + resources: + - '^/users/john/.*$' + subject: "user:john" + policy: two_factor + + - domain: dev.example.com + resources: + - '^/users/harry/.*$' + subject: "user:harry" + policy: two_factor + + - domain: '*.mail.example.com' + subject: "user:bob" + policy: two_factor + + - domain: dev.example.com + resources: + - '^/users/bob/.*$' + subject: "user:bob" + policy: two_factor + # Configuration of the authentication regulation mechanism. regulation: diff --git a/config.template.yml b/config.template.yml index 5b74c247..2bfcb28e 100644 --- a/config.template.yml +++ b/config.template.yml @@ -86,112 +86,96 @@ authentication_backend: ## path: ./users_database.yml -# Authentication methods -# -# Authentication methods can be defined per subdomain. -# There are currently two available methods: "single_factor" and "two_factor" -# -# Note: by default a domain uses "two_factor" method. -# -# Note: 'per_subdomain_methods' is a dictionary where keys must be subdomains and -# values must be one of the two possible methods. -# -# Note: 'per_subdomain_methods' is optional. -# -# Note: authentication_methods is optional. If it is not set all sub-domains -# are protected by two factors. -authentication_methods: - default_method: two_factor - per_subdomain_methods: - - - # Access Control # -# Access control is a set of rules you can use to restrict user access to certain -# resources. -# Any (apply to anyone), per-user or per-group rules can be defined. +# Access control is a list of rules defining the authorizations applied for one +# resource to users or group of users. # -# If 'access_control' is not defined, ACL rules are disabled and the `allow` default -# policy is applied, i.e., access is allowed to anyone. Otherwise restrictions follow +# If 'access_control' is not defined, ACL rules are disabled and the `bypass` +# rule is applied, i.e., access is allowed to anyone. Otherwise restrictions follow # the rules defined. -# -# Note: One can use the wildcard * to match any subdomain. +# +# Note: One can use the wildcard * to match any subdomain. # It must stand at the beginning of the pattern. (example: *.mydomain.com) -# -# Note: You must put the pattern in simple quotes when using the wildcard for the YAML +# +# Note: You must put patterns containing wildcards between simple quotes for the YAML # to be syntaxically correct. # -# Definition: A `rule` is an object with the following keys: `domain`, `policy` -# and `resources`. +# Definition: A `rule` is an object with the following keys: `domain`, `subject`, +# `policy` and `resources`. +# # - `domain` defines which domain or set of domains the rule applies to. -# - `policy` is the policy to apply to resources. It must be either `allow` or `deny`. -# - `resources` is a list of regular expressions that matches a set of resources to -# apply the policy to. # -# Note: Rules follow an order of priority defined as follows: -# In each category (`any`, `groups`, `users`), the latest rules have the highest -# priority. In other words, it means that if a given resource matches two rules in the -# same category, the latest one overrides the first one. -# Each category has also its own priority. That is, `users` has the highest priority, then -# `groups` and `any` has the lowest priority. It means if two rules in different categories -# match a given resource, the one in the category with the highest priority overrides the -# other one. +# - `subject` defines the subject to apply authorizations to. This parameter is +# optional and matching any user if not provided. If provided, the parameter +# represents either a user or a group. It should be of the form 'user:' +# or 'group:'. # +# - `policy` is the policy to apply to resources. It must be either `bypass`, +# `one_factor`, `two_factor` or `deny`. +# +# - `resources` is a list of regular expressions that matches a set of resources to +#  apply the policy to. This parameter is optional and matches any resource if not +# provided. +# +# Note: the order of the rules is important. The first policy matching +# (domain, resource, subject) applies. access_control: - # Default policy can either be `allow` or `deny`. - # It is the policy applied to any resource if it has not been overriden - # in the `any`, `groups` or `users` category. + # Default policy can either be `bypass`, `one_factor`, `two_factor` or `deny`. + # It is the policy applied to any resource if there is no policy to be applied + # to the user. default_policy: deny - # The rules that apply to anyone. - # The value is a list of rules. - any: + rules: + # Rules applied to everyone - domain: public.example.com policy: 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. - groups: - admin: - # All resources in all domains - - domain: '*.example.com' - 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: two_factor - resources: - - '^/groups/dev/.*$' - - # User-based rules. The key is a user name and the value - # is a list of rules. - users: - john: - - domain: dev.example.com - policy: two_factor - resources: - - '^/users/john/.*$' - harry: - - domain: dev.example.com - policy: two_factor - resources: - - '^/users/harry/.*$' - bob: - - domain: '*.mail.example.com' - policy: two_factor - - domain: 'dev.example.com' - policy: two_factor - resources: - - '^/users/bob/.*$' + + # Rules applied to 'admin' group + - domain: 'mx2.mail.example.com' + subject: 'group:admin' + policy: deny + - domain: '*.example.com' + subject: 'group:admin' + policy: two_factor + + # Rules applied to 'dev' group + - domain: dev.example.com + resources: + - '^/groups/dev/.*$' + subject: 'group:dev' + policy: two_factor + + # Rules applied to user 'john' + - domain: dev.example.com + resources: + - '^/users/john/.*$' + subject: 'user:john' + policy: two_factor + + + # Rules applied to user 'harry' + - domain: dev.example.com + resources: + - '^/users/harry/.*$' + subject: 'user:harry' + policy: two_factor + + # Rules applied to user 'bob' + - domain: '*.mail.example.com' + subject: 'user:bob' + policy: two_factor + - domain: 'dev.example.com' + resources: + - '^/users/bob/.*$' + subject: 'user:bob' + policy: two_factor # Configuration of session cookies -# +# # The session cookies identify the user once logged in. session: # The name of the session cookie. (default: authelia_session). @@ -199,7 +183,7 @@ session: # The secret to encrypt the session cookie. secret: unsecure_session_secret - + # The time in ms before the cookie expires and session is reset. expiration: 3600000 # 1 hour @@ -208,9 +192,9 @@ session: # The domain to protect. # Note: the authenticator must also be in that domain. If empty, the cookie - # is restricted to the subdomain of the issuer. + # is restricted to the subdomain of the issuer. domain: example.com - + # The redis connection details redis: host: redis @@ -223,12 +207,12 @@ session: # It bans the user if too many attempts are done in a short period of # time. regulation: - # The number of failed login attempts before user is banned. + # The number of failed login attempts before user is banned. # Set it to 0 to disable regulation. max_retries: 3 # The time range during which the user can attempt login before being banned. - # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. + # The user is banned if the authenticaction failed `max_retries` times in a `find_time` seconds window. find_time: 120 # The length of time before a banned user can login again. @@ -241,7 +225,7 @@ storage: # The directory where the DB files will be saved ## local: ## path: /var/lib/authelia/store - + # Settings to connect to mongo server mongo: url: mongodb://mongo @@ -261,13 +245,13 @@ notifier: ## filename: /tmp/authelia/notification.txt # Use your email account to send the notifications. You can use an app password. - # List of valid services can be found here: https://nodemailer.com/smtp/well-known/ + # List of valid services can be found here: https://nodemailer.com/smtp/well-known/ ## email: ## username: user@example.com ## password: yourpassword ## sender: admin@example.com ## service: gmail - + # Use a SMTP server for sending notifications smtp: username: test diff --git a/server/src/lib/IdentityCheckMiddleware.ts b/server/src/lib/IdentityCheckMiddleware.ts index fa525b4b..e72ea4db 100644 --- a/server/src/lib/IdentityCheckMiddleware.ts +++ b/server/src/lib/IdentityCheckMiddleware.ts @@ -102,7 +102,7 @@ export function get_start_validation(handler: IdentityValidable, let identity: Identity.Identity; return handler.preValidationInit(req) - .then(function (id: Identity.Identity) { + .then((id: Identity.Identity) => { identity = id; const email = identity.email; const userid = identity.userid; @@ -116,7 +116,7 @@ export function get_start_validation(handler: IdentityValidable, return createAndSaveToken(userid, handler.challenge(), vars.userDataStore); }) - .then(function (token: string) { + .then((token) => { const host = req.get("Host"); const link_url = util.format("https://%s%s?identity_token=%s", host, postValidationEndpoint, token); @@ -125,11 +125,11 @@ export function get_start_validation(handler: IdentityValidable, return vars.notifier.notify(identity.email, handler.mailSubject(), link_url); }) - .then(function () { + .then(() => { handler.preValidationResponse(req, res); return BluebirdPromise.resolve(); }) - .catch(Exceptions.IdentityError, function (err: Error) { + .catch(Exceptions.IdentityError, (err: Error) => { handler.preValidationResponse(req, res); return BluebirdPromise.resolve(); }) diff --git a/server/src/lib/authorization/Authorizer.spec.ts b/server/src/lib/authorization/Authorizer.spec.ts index 81477304..1027fb4b 100644 --- a/server/src/lib/authorization/Authorizer.spec.ts +++ b/server/src/lib/authorization/Authorizer.spec.ts @@ -25,9 +25,7 @@ describe("authorization/Authorizer", function () { beforeEach(function () { configuration = { default_policy: "deny", - any: [], - users: {}, - groups: {} + rules: [] }; authorizer = new Authorizer(configuration, winston); }); @@ -42,9 +40,10 @@ describe("authorization/Authorizer", function () { }); it("should control access when multiple domain matcher is provided", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "*.mail.example.com", policy: "two_factor", + subject: "user:user1", resources: [".*"] }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); @@ -54,9 +53,10 @@ describe("authorization/Authorizer", function () { }); it("should allow access to all resources when resources is not provided", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "*.mail.example.com", - policy: "two_factor" + policy: "two_factor", + subject: "user:user1" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("mx1.mail.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR); @@ -66,10 +66,11 @@ describe("authorization/Authorizer", function () { describe("check user rules", function () { it("should allow access when user has a matching allowing rule", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "home.example.com", policy: "two_factor", - resources: [".*"] + resources: [".*"], + subject: "user:user1" }]; 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); @@ -77,10 +78,11 @@ describe("authorization/Authorizer", function () { }); it("should deny to other users", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "home.example.com", policy: "two_factor", - resources: [".*"] + resources: [".*"], + subject: "user:user1" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user2", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/another/resource", "user2", ["group1"]), Level.DENY); @@ -88,10 +90,11 @@ describe("authorization/Authorizer", function () { }); it("should allow user access only to specific resources", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "home.example.com", policy: "two_factor", - resources: ["/private/.*", "^/begin", "/end$"] + resources: ["/private/.*", "^/begin", "/end$"], + subject: "user:user1" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); Assert.equal(authorizer.authorization("home.example.com", "/private", "user1", ["group1"]), Level.DENY); @@ -106,18 +109,21 @@ describe("authorization/Authorizer", function () { }); it("should allow access to multiple domains", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "home.example.com", policy: "two_factor", - resources: [".*"] + resources: [".*"], + subject: "user:user1" }, { domain: "home1.example.com", policy: "one_factor", - resources: [".*"] + resources: [".*"], + subject: "user:user1" }, { domain: "home2.example.com", policy: "deny", - resources: [".*"] + resources: [".*"], + subject: "user:user1" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.TWO_FACTOR); Assert.equal(authorizer.authorization("home1.example.com", "/", "user1", ["group1"]), Level.ONE_FACTOR); @@ -125,19 +131,22 @@ describe("authorization/Authorizer", function () { Assert.equal(authorizer.authorization("home3.example.com", "/", "user1", ["group1"]), Level.DENY); }); - it("should always apply latest rule", function () { - configuration.users["user1"] = [{ + it("should apply rules in order", function () { + configuration.rules = [{ domain: "home.example.com", - policy: "two_factor", - resources: ["^/my/.*"] + policy: "one_factor", + resources: ["/my/private/resource"], + subject: "user:user1" }, { domain: "home.example.com", policy: "deny", - resources: ["^/my/private/.*"] + resources: ["^/my/private/.*"], + subject: "user:user1" }, { domain: "home.example.com", - policy: "one_factor", - resources: ["/my/private/resource"] + policy: "two_factor", + resources: ["^/my/.*"], + subject: "user:user1" }]; Assert.equal(authorizer.authorization("home.example.com", "/my/poney", "user1", ["group1"]), Level.TWO_FACTOR); @@ -148,19 +157,21 @@ describe("authorization/Authorizer", function () { describe("check group rules", function () { it("should allow access when user is in group having a matching allowing rule", function () { - configuration.groups["group1"] = [{ + configuration.rules = [{ domain: "home.example.com", policy: "two_factor", - resources: ["^/$"] - }]; - configuration.groups["group2"] = [{ + resources: ["^/$"], + subject: "group:group1" + }, { domain: "home.example.com", policy: "one_factor", - resources: ["^/test$"] + resources: ["^/test$"], + subject: "group:group2" }, { domain: "home.example.com", policy: "deny", - resources: ["^/private$"] + resources: ["^/private$"], + subject: "group:group2" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1", "group2", "group3"]), Level.TWO_FACTOR); @@ -176,9 +187,9 @@ describe("authorization/Authorizer", function () { describe("check any rules", function () { it("should control access when any rules are defined", function () { - configuration.any = [{ + configuration.rules = [{ domain: "home.example.com", - policy: "two_factor", + policy: "bypass", resources: ["^/public$"] }, { domain: "home.example.com", @@ -186,11 +197,11 @@ describe("authorization/Authorizer", function () { resources: ["^/private$"] }]; Assert.equal(authorizer.authorization("home.example.com", "/public", "user1", - ["group1", "group2", "group3"]), Level.TWO_FACTOR); + ["group1", "group2", "group3"]), Level.BYPASS); 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); + ["group5"]), Level.BYPASS); Assert.equal(authorizer.authorization("home.example.com", "/private", "user4", ["group5"]), Level.DENY); }); @@ -208,10 +219,11 @@ describe("authorization/Authorizer", function () { }); it("should deny access to one resource when defined", function () { - configuration.users["user1"] = [{ + configuration.rules = [{ domain: "home.example.com", policy: "deny", - resources: ["/test"] + resources: ["/test"], + subject: "user:user1" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.BYPASS); Assert.equal(authorizer.authorization("home.example.com", "/test", "user1", ["group1"]), Level.DENY); @@ -229,39 +241,30 @@ describe("authorization/Authorizer", function () { // admin is in groups ["admins"] // john is in groups ["dev", "admin-private"] // harry is in groups ["dev"] - configuration.any = [{ + configuration.rules = [{ 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.*$"] + policy: "two_factor", + resources: [".*"], + subject: "group:admins" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/?.*"], + subject: "group:admin-private" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/john$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/private/harry"], + subject: "user:harry" }]; Assert.equal(authorizer.authorization("home.example.com", "/", "admin", ["admins"]), Level.TWO_FACTOR); @@ -275,8 +278,8 @@ describe("authorization/Authorizer", function () { 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", "/dev", "john", ["dev", "admin-private"]), Level.DENY); + Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]), Level.DENY); 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); @@ -284,7 +287,7 @@ describe("authorization/Authorizer", function () { 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", "harry", ["dev"]), Level.DENY); 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); @@ -292,49 +295,50 @@ describe("authorization/Authorizer", function () { 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"] = [{ + it("should allow when allowed at group level and denied at user level", function () { + configuration.rules = [{ domain: "home.example.com", policy: "deny", - resources: ["^/dev/bob$"] + resources: ["^/dev/bob$"], + subject: "user:john" + }, { + domain: "home.example.com", + policy: "two_factor", + resources: ["^/dev/?.*$"], + subject: "group: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.DENY); }); - it("should control access when allowed at 'any' level and denied at user level", function () { - configuration.any = [{ + it("should allow access when allowed at 'any' level and denied at user level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "user:john" + }, { domain: "home.example.com", policy: "two_factor", resources: ["^/dev/?.*$"] }]; - 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 = [{ + it("should allow access when allowed at 'any' level and denied at group level", function () { + configuration.rules = [{ + domain: "home.example.com", + policy: "deny", + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { domain: "home.example.com", policy: "two_factor", resources: ["^/dev/?.*$"] }]; - 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); @@ -344,17 +348,17 @@ describe("authorization/Authorizer", 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 = [{ + configuration.rules = [{ domain: "home.example.com", policy: "two_factor", - resources: ["^/dev/?.*$"] - }]; - configuration.groups["dev"] = [{ + resources: ["^/dev/?.*$"], + subject: "user:john" + }, { domain: "home.example.com", policy: "deny", - resources: ["^/dev/bob$"] - }]; - configuration.users["john"] = [{ + resources: ["^/dev/bob$"], + subject: "group:dev" + }, { domain: "home.example.com", policy: "two_factor", resources: ["^/dev/?.*$"] diff --git a/server/src/lib/authorization/Authorizer.ts b/server/src/lib/authorization/Authorizer.ts index e235a391..3cb640d3 100644 --- a/server/src/lib/authorization/Authorizer.ts +++ b/server/src/lib/authorization/Authorizer.ts @@ -24,6 +24,24 @@ function MatchResource(actualResource: string) { }; } +function MatchSubject(user: string, groups: string[]) { + return (rule: ACLRule) => { + // If no subject, matches anybody + if (!rule.subject) return true; + + if (rule.subject.startsWith("user:")) { + const ruleUser = rule.subject.split(":")[1]; + if (user == ruleUser) return true; + } + + if (rule.subject.startsWith("group:")) { + const ruleGroup = rule.subject.split(":")[1]; + if (groups.indexOf(ruleGroup) > -1) return true; + } + return false; + }; +} + export class Authorizer implements IAuthorizer { private logger: Winston; private readonly configuration: ACLConfiguration; @@ -33,39 +51,16 @@ export class Authorizer implements IAuthorizer { this.configuration = configuration; } - private getMatchingUserRules(user: string, domain: string, resource: string): ACLRule[] { - const userRules = this.configuration.users[user]; - if (!userRules) return []; - return userRules.filter(MatchDomain(domain)).filter(MatchResource(resource)); - } - - private getMatchingGroupRules(groups: string[], domain: string, resource: string): ACLRule[] { - const that = this; - // There is no ordering between group rules. That is, when a user belongs to 2 groups, there is no - // guarantee one set of rules has precedence on the other one. - const groupRules = groups.reduce(function (rules: ACLRule[], group: string) { - const groupRules = that.configuration.groups[group]; - if (groupRules) rules = rules.concat(groupRules); - return rules; - }, []); - return groupRules.filter(MatchDomain(domain)).filter(MatchResource(resource)); - } - - private getMatchingAllRules(domain: string, resource: string): ACLRule[] { - const rules = this.configuration.any; + private getMatchingRules(domain: string, resource: string, user: string, groups: string[]): ACLRule[] { + const rules = this.configuration.rules; if (!rules) return []; - return rules.filter(MatchDomain(domain)).filter(MatchResource(resource)); + return rules + .filter(MatchDomain(domain)) + .filter(MatchResource(resource)) + .filter(MatchSubject(user, groups)); } - 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]; - + private ruleToLevel(policy: string): Level { if (policy == "bypass") { return Level.BYPASS; } else if (policy == "one_factor") { @@ -75,4 +70,14 @@ export class Authorizer implements IAuthorizer { } return Level.DENY; } + + authorization(domain: string, resource: string, user: string, groups: string[]): Level { + if (!this.configuration) return Level.BYPASS; + + const rules = this.getMatchingRules(domain, resource, user, groups); + + return (rules.length > 0) + ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule + : this.ruleToLevel(this.configuration.default_policy); // otherwise use the default policy + } } \ No newline at end of file diff --git a/server/src/lib/configuration/ConfigurationParser.spec.ts b/server/src/lib/configuration/ConfigurationParser.spec.ts index 2baefc8a..60c0f618 100644 --- a/server/src/lib/configuration/ConfigurationParser.spec.ts +++ b/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -125,32 +125,26 @@ describe("configuration/ConfigurationParser", function () { const userConfig = buildYamlConfig(); userConfig.access_control = { default_policy: "deny", - any: [{ + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { domain: "public.example.com", policy: "two_factor" - }], - users: { - "user": [{ - domain: "www.example.com", - policy: "two_factor" - }] - }, - groups: {} + }] }; const config = ConfigurationParser.parse(userConfig); Assert.deepEqual(config.access_control, { default_policy: "deny", - any: [{ + rules: [{ + domain: "www.example.com", + policy: "two_factor", + subject: "user:user" + }, { domain: "public.example.com", policy: "two_factor" - }], - users: { - "user": [{ - domain: "www.example.com", - policy: "two_factor" - }] - }, - groups: {} + }] } as ACLConfiguration); }); @@ -161,9 +155,7 @@ describe("configuration/ConfigurationParser", function () { const config = ConfigurationParser.parse(userConfig); Assert.deepEqual(config.access_control, { default_policy: "bypass", - any: [], - users: {}, - groups: {} + rules: [] }); }); }); diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts index 0a4c02c7..d4a3093e 100644 --- a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -11,9 +11,7 @@ describe("configuration/SessionConfigurationBuilder", function () { const configuration: Configuration = { access_control: { default_policy: "deny", - any: [], - users: {}, - groups: {} + rules: [] }, totp: { issuer: "authelia.com" diff --git a/server/src/lib/configuration/schema/AclConfiguration.spec.ts b/server/src/lib/configuration/schema/AclConfiguration.spec.ts index 6b2f47f9..d1e2a03a 100644 --- a/server/src/lib/configuration/schema/AclConfiguration.spec.ts +++ b/server/src/lib/configuration/schema/AclConfiguration.spec.ts @@ -4,11 +4,31 @@ import Assert = require("assert"); describe("configuration/schema/AclConfiguration", function() { it("should complete ACLConfiguration", function() { const configuration: ACLConfiguration = {}; - const newConfiguration = complete(configuration); + const [newConfiguration, errors] = complete(configuration); Assert.deepEqual(newConfiguration.default_policy, "bypass"); - Assert.deepEqual(newConfiguration.any, []); - Assert.deepEqual(newConfiguration.groups, {}); - Assert.deepEqual(newConfiguration.users, {}); + Assert.deepEqual(newConfiguration.rules, []); + }); + + it("should return errors when subject is not good", function() { + const configuration: ACLConfiguration = { + default_policy: "deny", + rules: [{ + domain: "dev.example.com", + subject: "user:abc", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "user:def", + policy: "bypass" + }, { + domain: "dev.example.com", + subject: "badkey:abc", + policy: "bypass" + }] + }; + const [newConfiguration, errors] = complete(configuration); + + Assert.deepEqual(errors, ["Rule 2 has wrong subject. It should be starting with user: or group:."]); }); }); \ No newline at end of file diff --git a/server/src/lib/configuration/schema/AclConfiguration.ts b/server/src/lib/configuration/schema/AclConfiguration.ts index e29dceb2..40401dd6 100644 --- a/server/src/lib/configuration/schema/AclConfiguration.ts +++ b/server/src/lib/configuration/schema/AclConfiguration.ts @@ -3,22 +3,17 @@ export type ACLPolicy = "deny" | "bypass" | "one_factor" | "two_factor"; export type ACLRule = { domain: string; - policy: ACLPolicy; resources?: string[]; + subject?: string; + policy: ACLPolicy; }; -export type ACLDefaultRules = ACLRule[]; -export type ACLGroupsRules = { [group: string]: ACLRule[]; }; -export type ACLUsersRules = { [user: string]: ACLRule[]; }; - export interface ACLConfiguration { default_policy?: ACLPolicy; - any?: ACLDefaultRules; - groups?: ACLGroupsRules; - users?: ACLUsersRules; + rules?: ACLRule[]; } -export function complete(configuration: ACLConfiguration): ACLConfiguration { +export function complete(configuration: ACLConfiguration): [ACLConfiguration, string[]] { const newConfiguration: ACLConfiguration = (configuration) ? JSON.parse(JSON.stringify(configuration)) : {}; @@ -26,17 +21,21 @@ export function complete(configuration: ACLConfiguration): ACLConfiguration { newConfiguration.default_policy = "bypass"; } - if (!newConfiguration.any) { - newConfiguration.any = []; + if (!newConfiguration.rules) { + newConfiguration.rules = []; } - if (!newConfiguration.groups) { - newConfiguration.groups = {}; + if (newConfiguration.rules.length > 0) { + const errors: string[] = []; + newConfiguration.rules.forEach((r, idx) => { + if (r.subject && !r.subject.match(/^(user|group):[a-zA-Z0-9]+$/)) { + errors.push(`Rule ${idx} has wrong subject. It should be starting with user: or group:.`); + } + }); + if (errors.length > 0) { + return [newConfiguration, errors]; + } } - if (!newConfiguration.users) { - newConfiguration.users = {}; - } - - return newConfiguration; + return [newConfiguration, []]; } \ 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 9798bc83..8d16a5fb 100644 --- a/server/src/lib/configuration/schema/Configuration.ts +++ b/server/src/lib/configuration/schema/Configuration.ts @@ -27,9 +27,13 @@ export function complete( JSON.stringify(configuration)); const errors: string[] = []; - newConfiguration.access_control = - AclConfigurationComplete( - newConfiguration.access_control); + const [acls, aclsErrors] = AclConfigurationComplete( + newConfiguration.access_control); + + newConfiguration.access_control = acls; + if (aclsErrors.length > 0) { + errors.concat(aclsErrors); + } const [backend, error] = AuthenticationBackendComplete( diff --git a/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts b/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts index 689c4233..6c576e8e 100644 --- a/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts +++ b/server/src/lib/configuration/schema/NotifierConfiguration.spec.ts @@ -6,12 +6,12 @@ describe("configuration/schema/NotifierConfiguration", function() { const configuration: NotifierConfiguration = {}; const [newConfiguration, error] = complete(configuration); - Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}) + Assert.deepEqual(newConfiguration.filesystem, {filename: "/tmp/authelia/notification.txt"}); }); it("should ensure correct key is provided", function() { const configuration = { - abc: 'badvalue' + abc: "badvalue" }; const [newConfiguration, error] = complete(configuration as any); diff --git a/server/src/lib/routes/verify/get.spec.ts b/server/src/lib/routes/verify/get.spec.ts index 376fa622..67cf19fb 100644 --- a/server/src/lib/routes/verify/get.spec.ts +++ b/server/src/lib/routes/verify/get.spec.ts @@ -246,7 +246,7 @@ describe("routes/verify/get", function () { it("should fail when endpoint is protected by two factors", function () { mocks.authorizer.authorizationMock.returns(AuthorizationLevel.TWO_FACTOR); mocks.config.access_control.default_policy = "one_factor"; - mocks.config.access_control.any = [{ + mocks.config.access_control.rules = [{ domain: "secret.example.com", policy: "two_factor" }]; diff --git a/server/src/lib/storage/mongo/MongoCollection.spec.ts b/server/src/lib/storage/mongo/MongoCollection.spec.ts index 9838c21c..74a773a1 100644 --- a/server/src/lib/storage/mongo/MongoCollection.spec.ts +++ b/server/src/lib/storage/mongo/MongoCollection.spec.ts @@ -10,7 +10,7 @@ describe("storage/mongo/MongoCollection", function () { let mongoClientStub: MongoClientStub; let findStub: Sinon.SinonStub; let findOneStub: Sinon.SinonStub; - let insertStub: Sinon.SinonStub; + let insertOneStub: Sinon.SinonStub; let updateStub: Sinon.SinonStub; let removeStub: Sinon.SinonStub; let countStub: Sinon.SinonStub; @@ -21,7 +21,7 @@ describe("storage/mongo/MongoCollection", function () { mongoCollectionStub = Sinon.createStubInstance(require("mongodb").Collection as any); findStub = mongoCollectionStub.find as Sinon.SinonStub; findOneStub = mongoCollectionStub.findOne as Sinon.SinonStub; - insertStub = mongoCollectionStub.insert as Sinon.SinonStub; + insertOneStub = mongoCollectionStub.insertOne as Sinon.SinonStub; updateStub = mongoCollectionStub.update as Sinon.SinonStub; removeStub = mongoCollectionStub.remove as Sinon.SinonStub; countStub = mongoCollectionStub.count as Sinon.SinonStub; @@ -63,11 +63,11 @@ describe("storage/mongo/MongoCollection", function () { describe("insert", function () { it("should insert a document in the collection", function () { const collection = new MongoCollection(COLLECTION_NAME, mongoClientStub); - insertStub.returns(BluebirdPromise.resolve({})); + insertOneStub.returns(BluebirdPromise.resolve({})); return collection.insert({ key: "KEY" }) .then(function () { - Assert(insertStub.calledWith({ key: "KEY" })); + Assert(insertOneStub.calledWith({ key: "KEY" })); }); }); }); diff --git a/server/src/lib/storage/mongo/MongoCollection.ts b/server/src/lib/storage/mongo/MongoCollection.ts index f23f75ba..9771389f 100644 --- a/server/src/lib/storage/mongo/MongoCollection.ts +++ b/server/src/lib/storage/mongo/MongoCollection.ts @@ -40,7 +40,7 @@ export class MongoCollection implements ICollection { insert(document: any): Bluebird { return this.collection() - .then((collection) => collection.insert(document)); + .then((collection) => collection.insertOne(document)); } count(query: any): Bluebird { diff --git a/test/features/access-control.feature b/test/features/access-control.feature index 5539b559..0e513ea1 100644 --- a/test/features/access-control.feature +++ b/test/features/access-control.feature @@ -33,7 +33,7 @@ Feature: User has access restricted access to domains And I have access to "https://dev.example.com:8080/users/bob/secret.html" And I have no access to "https://admin.example.com:8080/secret.html" And I have access to "https://mx1.mail.example.com:8080/secret.html" - And I have no access to "https://single_factor.example.com:8080/secret.html" + And I have access to "https://single_factor.example.com:8080/secret.html" And I have access to "https://mx2.mail.example.com:8080/secret.html" @need-registered-user-harry @@ -51,5 +51,5 @@ Feature: User has access restricted access to domains And I have no access to "https://dev.example.com:8080/users/bob/secret.html" And I have no access to "https://admin.example.com:8080/secret.html" And I have no access to "https://mx1.mail.example.com:8080/secret.html" - And I have no access to "https://single_factor.example.com:8080/secret.html" + And I have access to "https://single_factor.example.com:8080/secret.html" And I have no access to "https://mx2.mail.example.com:8080/secret.html" diff --git a/test/features/restrictions.feature b/test/features/restrictions.feature index 2e33371d..97c85a34 100644 --- a/test/features/restrictions.feature +++ b/test/features/restrictions.feature @@ -14,24 +14,3 @@ Feature: Non authenticated users have no access to certain pages | https://login.example.com:8080/api/u2f/sign | 401 | POST | | https://login.example.com:8080/api/u2f/register_request | 401 | GET | | https://login.example.com:8080/api/u2f/register | 401 | POST | - - - @needs-single_factor-config - @need-registered-user-john - Scenario: User does not have acces to second factor related endpoints when in single factor mode - Given I post "https://login.example.com:8080/api/firstfactor" with body: - | key | value | - | username | john | - | password | password | - Then I get the following status code when requesting: - | url | code | method | - | https://login.example.com:8080/secondfactor | 401 | GET | - | https://login.example.com:8080/secondfactor/u2f/identity/start | 401 | GET | - | https://login.example.com:8080/secondfactor/u2f/identity/finish | 401 | GET | - | https://login.example.com:8080/secondfactor/totp/identity/start | 401 | GET | - | https://login.example.com:8080/secondfactor/totp/identity/finish | 401 | GET | - | https://login.example.com:8080/api/totp | 401 | POST | - | https://login.example.com:8080/api/u2f/sign_request | 401 | GET | - | https://login.example.com:8080/api/u2f/sign | 401 | POST | - | https://login.example.com:8080/api/u2f/register_request | 401 | GET | - | https://login.example.com:8080/api/u2f/register | 401 | POST | \ No newline at end of file diff --git a/test/features/single-factor-domain.feature b/test/features/single-factor-domain.feature index db13bb94..9fee7be9 100644 --- a/test/features/single-factor-domain.feature +++ b/test/features/single-factor-domain.feature @@ -13,3 +13,4 @@ Feature: User can access certain subdomains with single factor Scenario: User can login using basic authentication When I request "https://single_factor.example.com:8080/secret.html" with username "john" and password "password" using basic authentication Then I receive the secret page + diff --git a/test/features/single-factor-only-server.feature b/test/features/single-factor-only-server.feature deleted file mode 100644 index 4d3fc42f..00000000 --- a/test/features/single-factor-only-server.feature +++ /dev/null @@ -1,16 +0,0 @@ -@needs-single_factor-config -Feature: Server is configured as a single factor only server - - @need-registered-user-john - Scenario: User is redirected to service after first factor if allowed - When I visit "https://login.example.com:8080/?rd=https://public.example.com:8080/secret.html" - And I login with user "john" and password "password" - Then I'm redirected to "https://public.example.com:8080/secret.html" - - @need-registered-user-john - Scenario: User is correctly redirected according to default redirection URL - When I visit "https://login.example.com:8080" - And I login with user "john" and password "password" - Then I'm redirected to "https://login.example.com:8080/loggedin" - And I sleep for 5 seconds - Then I'm redirected to "https://home.example.com:8080/" diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts index cf7eb0c1..3ab37390 100644 --- a/test/features/step_definitions/restrictions.ts +++ b/test/features/step_definitions/restrictions.ts @@ -33,7 +33,6 @@ function requestAndExpectStatusCode(ctx: any, url: string, method: string, Assert.equal(statusCode, expectedStatusCode); } catch (e) { - console.log(url); console.log("%s (actual) != %s (expected)", statusCode, expectedStatusCode); throw e; From b53d16d8a1f14cae05bf025418ababa9a862bb07 Mon Sep 17 00:00:00 2001 From: Clement Michaud Date: Thu, 25 Oct 2018 21:05:07 +0200 Subject: [PATCH 4/4] Introduce Subject and Object in authorization module. --- .../src/lib/authorization/Authorizer.spec.ts | 176 +++++++++--------- server/src/lib/authorization/Authorizer.ts | 20 +- .../lib/authorization/AuthorizerStub.spec.ts | 6 +- server/src/lib/authorization/IAuthorizer.ts | 4 +- server/src/lib/authorization/Object.ts | 5 + server/src/lib/authorization/Subject.ts | 5 + server/src/lib/routes/firstfactor/post.ts | 3 +- .../src/lib/routes/verify/access_control.ts | 10 +- .../src/lib/routes/verify/get_basic_auth.ts | 5 - .../lib/routes/verify/get_session_cookie.ts | 12 +- .../features/step_definitions/restrictions.ts | 5 - 11 files changed, 128 insertions(+), 123 deletions(-) create mode 100644 server/src/lib/authorization/Object.ts create mode 100644 server/src/lib/authorization/Subject.ts diff --git a/server/src/lib/authorization/Authorizer.spec.ts b/server/src/lib/authorization/Authorizer.spec.ts index 1027fb4b..58681404 100644 --- a/server/src/lib/authorization/Authorizer.spec.ts +++ b/server/src/lib/authorization/Authorizer.spec.ts @@ -14,10 +14,10 @@ describe("authorization/Authorizer", 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc"}, {user: "user1", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1", "group2"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "admin.example.com", resource: "/"}, {user: "user3", groups: ["group3"]}), Level.BYPASS); }); }); @@ -36,7 +36,7 @@ describe("authorization/Authorizer", function () { }); it("should deny access when no rule is provided", function () { - Assert.equal(authorizer.authorization("home.example.com", "/", "user1", ["group1"]), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); }); it("should control access when multiple domain matcher is provided", function () { @@ -46,10 +46,10 @@ describe("authorization/Authorizer", function () { subject: "user:user1", 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); }); it("should allow access to all resources when resources is not provided", function () { @@ -58,10 +58,10 @@ describe("authorization/Authorizer", function () { policy: "two_factor", subject: "user:user1" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "mx1.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mx1.server.mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "mail.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); }); describe("check user rules", function () { @@ -72,9 +72,9 @@ describe("authorization/Authorizer", function () { resources: [".*"], subject: "user:user1" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); }); it("should deny to other users", function () { @@ -84,9 +84,9 @@ describe("authorization/Authorizer", function () { resources: [".*"], subject: "user:user1" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/another/resource"}, {user: "user2", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, {user: "user2", groups: ["group1"]}), Level.DENY); }); it("should allow user access only to specific resources", function () { @@ -96,16 +96,16 @@ describe("authorization/Authorizer", function () { resources: ["/private/.*", "^/begin", "/end$"], subject: "user:user1" }]; - 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({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/middle/private/class"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization("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({domain: "home.example.com", resource: "/begin"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/not/begin"}, {user: "user1", groups: ["group1"]}), Level.DENY); - Assert.equal(authorizer.authorization("home.example.com", "/abc/end", "user1", ["group1"]), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization("home.example.com", "/abc/end/x", "user1", ["group1"]), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/abc/end/x"}, {user: "user1", groups: ["group1"]}), Level.DENY); }); it("should allow access to multiple domains", function () { @@ -125,10 +125,10 @@ describe("authorization/Authorizer", function () { resources: [".*"], subject: "user:user1" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home1.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home2.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home3.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.DENY); }); it("should apply rules in order", function () { @@ -149,9 +149,9 @@ describe("authorization/Authorizer", function () { subject: "user:user1" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/poney"}, {user: "user1", groups: ["group1"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/duck"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/my/private/resource"}, {user: "user1", groups: ["group1"]}), Level.ONE_FACTOR); }); }); @@ -173,14 +173,14 @@ describe("authorization/Authorizer", function () { resources: ["^/private$"], subject: "group:group2" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.ONE_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "another.home.example.com", resource: "/"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); }); }); }); @@ -196,14 +196,14 @@ describe("authorization/Authorizer", function () { policy: "deny", resources: ["^/private$"] }]; - Assert.equal(authorizer.authorization("home.example.com", "/public", "user1", - ["group1", "group2", "group3"]), Level.BYPASS); - 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.BYPASS); - Assert.equal(authorizer.authorization("home.example.com", "/private", "user4", - ["group5"]), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user1", groups: ["group1", "group2", "group3"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, + {user: "user4", groups: ["group5"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private"}, + {user: "user4", groups: ["group5"]}), Level.DENY); }); }); @@ -213,9 +213,9 @@ describe("authorization/Authorizer", function () { }); 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); }); it("should deny access to one resource when defined", function () { @@ -225,9 +225,9 @@ describe("authorization/Authorizer", function () { resources: ["/test"], subject: "user:user1" }]; - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/test"}, {user: "user1", groups: ["group1"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "user1", groups: ["group1"]}), Level.BYPASS); }); }); @@ -267,32 +267,32 @@ describe("authorization/Authorizer", function () { subject: "user:harry" }]; - 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({domain: "home.example.com", resource: "/"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "admin", groups: ["admins"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization("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.DENY); - Assert.equal(authorizer.authorization("home.example.com", "/dev/bob", "john", ["dev", "admin-private"]), Level.DENY); - 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({domain: "home.example.com", resource: "/"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "john", groups: ["dev", "admin-private"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "john", groups: ["dev", "admin-private"]}), Level.TWO_FACTOR); - Assert.equal(authorizer.authorization("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.DENY); - 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/public"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/admin"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/josh"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/john"}, {user: "harry", groups: ["dev"]}), Level.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/private/harry"}, {user: "harry", groups: ["dev"]}), Level.TWO_FACTOR); }); it("should allow when allowed at group level and denied at user level", function () { @@ -308,8 +308,8 @@ describe("authorization/Authorizer", function () { subject: "group: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.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); }); it("should allow access when allowed at 'any' level and denied at user level", function () { @@ -324,8 +324,8 @@ describe("authorization/Authorizer", function () { 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.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); }); it("should allow access when allowed at 'any' level and denied at group level", function () { @@ -340,8 +340,8 @@ describe("authorization/Authorizer", function () { 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.DENY); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.DENY); }); it("should respect rules precedence", function () { @@ -364,8 +364,8 @@ describe("authorization/Authorizer", function () { 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); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/john"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); + Assert.equal(authorizer.authorization({domain: "home.example.com", resource: "/dev/bob"}, {user: "john", groups: ["dev"]}), Level.TWO_FACTOR); }); }); }); diff --git a/server/src/lib/authorization/Authorizer.ts b/server/src/lib/authorization/Authorizer.ts index 3cb640d3..889b7ec2 100644 --- a/server/src/lib/authorization/Authorizer.ts +++ b/server/src/lib/authorization/Authorizer.ts @@ -4,6 +4,8 @@ import { IAuthorizer } from "./IAuthorizer"; import { Winston } from "../../../types/Dependencies"; import { MultipleDomainMatcher } from "./MultipleDomainMatcher"; import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; function MatchDomain(actualDomain: string) { return function (rule: ACLRule): boolean { @@ -24,19 +26,19 @@ function MatchResource(actualResource: string) { }; } -function MatchSubject(user: string, groups: string[]) { +function MatchSubject(subject: Subject) { return (rule: ACLRule) => { // If no subject, matches anybody if (!rule.subject) return true; if (rule.subject.startsWith("user:")) { const ruleUser = rule.subject.split(":")[1]; - if (user == ruleUser) return true; + if (subject.user == ruleUser) return true; } if (rule.subject.startsWith("group:")) { const ruleGroup = rule.subject.split(":")[1]; - if (groups.indexOf(ruleGroup) > -1) return true; + if (subject.groups.indexOf(ruleGroup) > -1) return true; } return false; }; @@ -51,13 +53,13 @@ export class Authorizer implements IAuthorizer { this.configuration = configuration; } - private getMatchingRules(domain: string, resource: string, user: string, groups: string[]): ACLRule[] { + private getMatchingRules(object: Object, subject: Subject): ACLRule[] { const rules = this.configuration.rules; if (!rules) return []; return rules - .filter(MatchDomain(domain)) - .filter(MatchResource(resource)) - .filter(MatchSubject(user, groups)); + .filter(MatchDomain(object.domain)) + .filter(MatchResource(object.resource)) + .filter(MatchSubject(subject)); } private ruleToLevel(policy: string): Level { @@ -71,10 +73,10 @@ export class Authorizer implements IAuthorizer { return Level.DENY; } - authorization(domain: string, resource: string, user: string, groups: string[]): Level { + authorization(object: Object, subject: Subject): Level { if (!this.configuration) return Level.BYPASS; - const rules = this.getMatchingRules(domain, resource, user, groups); + const rules = this.getMatchingRules(object, subject); return (rules.length > 0) ? this.ruleToLevel(rules[0].policy) // extract the policy of the first matching rule diff --git a/server/src/lib/authorization/AuthorizerStub.spec.ts b/server/src/lib/authorization/AuthorizerStub.spec.ts index 3b8ece28..9bd6f4a8 100644 --- a/server/src/lib/authorization/AuthorizerStub.spec.ts +++ b/server/src/lib/authorization/AuthorizerStub.spec.ts @@ -1,6 +1,8 @@ import Sinon = require("sinon"); import { IAuthorizer } from "./IAuthorizer"; import { Level } from "./Level"; +import { Object } from "./Object"; +import { Subject } from "./Subject"; export class AuthorizerStub implements IAuthorizer { authorizationMock: Sinon.SinonStub; @@ -9,7 +11,7 @@ export class AuthorizerStub implements IAuthorizer { this.authorizationMock = Sinon.stub(); } - authorization(domain: string, resource: string, user: string, groups: string[]): Level { - return this.authorizationMock(domain, resource, user, groups); + authorization(object: Object, subject: Subject): Level { + return this.authorizationMock(object, subject); } } diff --git a/server/src/lib/authorization/IAuthorizer.ts b/server/src/lib/authorization/IAuthorizer.ts index 1b5caabc..fe7ba367 100644 --- a/server/src/lib/authorization/IAuthorizer.ts +++ b/server/src/lib/authorization/IAuthorizer.ts @@ -1,5 +1,7 @@ import { Level } from "./Level"; +import { Subject } from "./Subject"; +import { Object } from "./Object"; export interface IAuthorizer { - authorization(domain: string, resource: string, user: string, groups: string[]): Level; + authorization(object: Object, subject: Subject): Level; } \ No newline at end of file diff --git a/server/src/lib/authorization/Object.ts b/server/src/lib/authorization/Object.ts new file mode 100644 index 00000000..5411b0d2 --- /dev/null +++ b/server/src/lib/authorization/Object.ts @@ -0,0 +1,5 @@ + +export interface Object { + domain: string; + resource: string; +} \ No newline at end of file diff --git a/server/src/lib/authorization/Subject.ts b/server/src/lib/authorization/Subject.ts new file mode 100644 index 00000000..310d6b4c --- /dev/null +++ b/server/src/lib/authorization/Subject.ts @@ -0,0 +1,5 @@ + +export interface Subject { + user: string; + groups: string[]; +} \ No newline at end of file diff --git a/server/src/lib/routes/firstfactor/post.ts b/server/src/lib/routes/firstfactor/post.ts index ba45c3ec..565681d6 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -59,7 +59,8 @@ export default function (vars: ServerVariables) { const decomposition = URLDecomposer.fromUrl(redirectUrl); const authorizationLevel = (decomposition) ? vars.authorizer.authorization( - decomposition.domain, decomposition.path, username, groups) + {domain: decomposition.domain, resource: decomposition.path}, + {user: username, groups: groups}) : AuthorizationLevel.TWO_FACTOR; if (emails.length > 0) diff --git a/server/src/lib/routes/verify/access_control.ts b/server/src/lib/routes/verify/access_control.ts index 86e74029..136239ae 100644 --- a/server/src/lib/routes/verify/access_control.ts +++ b/server/src/lib/routes/verify/access_control.ts @@ -28,22 +28,22 @@ function isAuthorized( export default function ( req: Express.Request, vars: ServerVariables, - domain: string, path: string, - username: string, groups: string[], + domain: string, resource: string, + user: string, groups: string[], authenticationLevel: AuthenticationLevel) { return new BluebirdPromise(function (resolve, reject) { const authorizationLevel = vars.authorizer - .authorization(domain, path, username, groups); + .authorization({domain, resource}, {user, groups}); 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))); + Util.format("User %s is not authorized to access %s%s", user, domain, resource))); return; } reject(new Exceptions.NotAuthenticatedError(Util.format( - "User '%s' is not sufficiently authenticated.", username, domain, path))); + "User '%s' is not sufficiently authorized to access %s%s.", user, domain, resource))); return; } resolve(); diff --git a/server/src/lib/routes/verify/get_basic_auth.ts b/server/src/lib/routes/verify/get_basic_auth.ts index c57a0125..af23c76c 100644 --- a/server/src/lib/routes/verify/get_basic_auth.ts +++ b/server/src/lib/routes/verify/get_basic_auth.ts @@ -4,11 +4,6 @@ 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"; diff --git a/server/src/lib/routes/verify/get_session_cookie.ts b/server/src/lib/routes/verify/get_session_cookie.ts index dc7453ad..07034481 100644 --- a/server/src/lib/routes/verify/get_session_cookie.ts +++ b/server/src/lib/routes/verify/get_session_cookie.ts @@ -14,9 +14,6 @@ import { 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"; - function verify_inactivity(req: Express.Request, authSession: AuthenticationSession, configuration: Configuration, logger: IRequestLogger) @@ -54,18 +51,19 @@ export default function (req: Express.Request, res: Express.Response, if (!authSession.userid) { return BluebirdPromise.reject(new Exceptions.AccessDeniedError( - Util.format("%s: %s.", FIRST_FACTOR_NOT_VALIDATED_MESSAGE, - "userid is missing"))); + "userid is missing")); } - const originalUrl = ObjectPath.get(req, "headers.x-original-url"); + const originalUrl = ObjectPath.get( + req, "headers.x-original-url"); const originalUri = ObjectPath.get(req, "headers.x-original-uri"); 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); + return AccessControl(req, vars, d.domain, d.path, username, groups, + authSession.authentication_level); }) .then(() => { return verify_inactivity(req, authSession, diff --git a/test/features/step_definitions/restrictions.ts b/test/features/step_definitions/restrictions.ts index 3ab37390..db218ab2 100644 --- a/test/features/step_definitions/restrictions.ts +++ b/test/features/step_definitions/restrictions.ts @@ -12,11 +12,6 @@ Then("I get an error {int}", function (code: number) { return this.getErrorPage(code); }); -When("I request {string} with method {string}", - function (url: string, method: string) { - const that = this; - }); - function requestAndExpectStatusCode(ctx: any, url: string, method: string, expectedStatusCode: number) { return Request(url, {