From 9dab40c2ce0c4a691cab317417d7e8fedfcf92f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Michaud?= Date: Sun, 26 Aug 2018 10:30:43 +0200 Subject: [PATCH] Add support for users database on disk. (#262) In order to simplify the deployment of Authelia for testing, LDAP is now optional made optional thanks to users database stored in a file. One can update the file manually even while Authelia is running. With this feature the minimal configuration requires only two components: Authelia and nginx. The users database is obviously made for development environments only as it prevents Authelia to be scaled to more than one instance. Note: Configuration has been updated. Key `ldap` has been nested in `authentication_backend`. --- .gitignore | 2 + config.minimal.yml | 15 +- config.template.yml | 93 +++++--- docker-compose.minimal.yml | 3 +- server/src/lib/Server.spec.ts | 12 +- server/src/lib/Server.ts | 5 +- server/src/lib/ServerVariables.ts | 2 +- server/src/lib/ServerVariablesInitializer.ts | 56 +++-- .../lib/ServerVariablesMockBuilder.spec.ts | 30 +-- .../backends/GroupsAndEmails.ts | 5 + .../backends}/IUsersDatabase.ts | 3 +- .../backends/IUsersDatabaseStub.spec.ts} | 4 +- .../backends/file/FileUsersDatabase.spec.ts | 224 ++++++++++++++++++ .../backends/file/FileUsersDatabase.ts | 182 ++++++++++++++ .../backends/file/ReadWriteQueue.ts | 60 +++++ .../backends}/ldap/ISession.ts | 5 - .../backends}/ldap/ISessionFactory.ts | 0 .../backends}/ldap/LdapUsersDatabase.spec.ts | 0 .../backends}/ldap/LdapUsersDatabase.ts | 9 +- .../backends}/ldap/SafeSession.spec.ts | 0 .../backends}/ldap/SafeSession.ts | 2 +- .../backends}/ldap/Sanitizer.spec.ts | 0 .../backends}/ldap/Sanitizer.ts | 0 .../backends}/ldap/Session.spec.ts | 2 +- .../backends}/ldap/Session.ts | 10 +- .../backends}/ldap/SessionFactory.ts | 2 +- .../backends}/ldap/SessionFactoryStub.spec.ts | 0 .../backends}/ldap/SessionStub.spec.ts | 2 +- .../backends}/ldap/connector/Connector.ts | 2 +- .../ldap/connector/ConnectorFactory.ts | 2 +- .../connector/ConnectorFactoryStub.spec.ts | 3 +- .../ldap/connector/ConnectorStub.spec.ts | 3 +- .../backends}/ldap/connector/IConnector.ts | 1 - .../ldap/connector/IConnectorFactory.ts | 0 .../configuration/ConfigurationParser.spec.ts | 16 +- .../SessionConfigurationBuilder.spec.ts | 48 ++-- ...AuthenticationBackendConfiguration.spec.ts | 11 + .../AuthenticationBackendConfiguration.ts | 25 ++ .../lib/configuration/schema/Configuration.ts | 18 +- .../schema/FileUsersDatabaseConfiguration.ts | 4 + server/src/lib/routes/firstfactor/post.ts | 2 +- .../routes/password-reset/form/post.spec.ts | 2 +- .../identity/PasswordResetHandler.ts | 2 +- server/src/lib/utils/HashGenerator.spec.ts | 4 +- server/src/lib/utils/HashGenerator.ts | 6 +- test/helpers/click-on-link.ts | 10 + test/helpers/fill-field.ts | 10 + test/helpers/get-identity-link.ts | 34 +++ test/helpers/register-totp.ts | 38 +-- test/minimal-config/00-suite.ts | 17 +- test/minimal-config/reset_password.ts | 43 ++++ users_database.yml | 29 +++ 52 files changed, 865 insertions(+), 193 deletions(-) create mode 100644 server/src/lib/authentication/backends/GroupsAndEmails.ts rename server/src/lib/{ldap => authentication/backends}/IUsersDatabase.ts (85%) rename server/src/lib/{ldap/UsersDatabaseStub.spec.ts => authentication/backends/IUsersDatabaseStub.spec.ts} (89%) create mode 100644 server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts create mode 100644 server/src/lib/authentication/backends/file/FileUsersDatabase.ts create mode 100644 server/src/lib/authentication/backends/file/ReadWriteQueue.ts rename server/src/lib/{ => authentication/backends}/ldap/ISession.ts (83%) rename server/src/lib/{ => authentication/backends}/ldap/ISessionFactory.ts (100%) rename server/src/lib/{ => authentication/backends}/ldap/LdapUsersDatabase.spec.ts (100%) rename server/src/lib/{ => authentication/backends}/ldap/LdapUsersDatabase.ts (91%) rename server/src/lib/{ => authentication/backends}/ldap/SafeSession.spec.ts (100%) rename server/src/lib/{ => authentication/backends}/ldap/SafeSession.ts (96%) rename server/src/lib/{ => authentication/backends}/ldap/Sanitizer.spec.ts (100%) rename server/src/lib/{ => authentication/backends}/ldap/Sanitizer.ts (100%) rename server/src/lib/{ => authentication/backends}/ldap/Session.spec.ts (97%) rename server/src/lib/{ => authentication/backends}/ldap/Session.ts (94%) rename server/src/lib/{ => authentication/backends}/ldap/SessionFactory.ts (92%) rename server/src/lib/{ => authentication/backends}/ldap/SessionFactoryStub.spec.ts (100%) rename server/src/lib/{ => authentication/backends}/ldap/SessionStub.spec.ts (95%) rename server/src/lib/{ => authentication/backends}/ldap/connector/Connector.ts (97%) rename server/src/lib/{ => authentication/backends}/ldap/connector/ConnectorFactory.ts (83%) rename server/src/lib/{ => authentication/backends}/ldap/connector/ConnectorFactoryStub.spec.ts (99%) rename server/src/lib/{ => authentication/backends}/ldap/connector/ConnectorStub.spec.ts (99%) rename server/src/lib/{ => authentication/backends}/ldap/connector/IConnector.ts (99%) rename server/src/lib/{ => authentication/backends}/ldap/connector/IConnectorFactory.ts (100%) create mode 100644 server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts create mode 100644 server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts create mode 100644 server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts create mode 100644 test/helpers/click-on-link.ts create mode 100644 test/helpers/fill-field.ts create mode 100644 test/helpers/get-identity-link.ts create mode 100644 test/minimal-config/reset_password.ts create mode 100644 users_database.yml diff --git a/.gitignore b/.gitignore index 61eb015ec..eb1f17538 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ dist/ example/ldap/private.ldif Configuration.schema.json + +users_database.test.yml diff --git a/config.minimal.yml b/config.minimal.yml index 685885f53..2daa4764e 100644 --- a/config.minimal.yml +++ b/config.minimal.yml @@ -2,17 +2,10 @@ # Authelia minimal configuration # ############################################################### -ldap: - url: ldap://openldap - base_dn: dc=example,dc=com - - additional_users_dn: ou=users - additional_groups_dn: ou=groups - - groups_filter: (&(member={dn})(objectclass=groupOfNames)) - - user: cn=admin,dc=example,dc=com - password: password +authentication_backend: + file: + # The path to the database file. The file is at the root of the repo. + path: /etc/authelia/users_database.yml session: # The secret to encrypt the session cookies with. diff --git a/config.template.yml b/config.template.yml index 70889e4af..275a83ef2 100644 --- a/config.template.yml +++ b/config.template.yml @@ -29,43 +29,61 @@ default_redirection_url: https://home.example.com:8080/ totp: issuer: authelia.com - -# LDAP configuration +# The authentication backend to use for verifying user passwords +# and retrieve information such as email address and groups +# users belong to. # -# Example: for user john, the DN will be cn=john,ou=users,dc=example,dc=com -ldap: - # The url of the ldap server - url: ldap://openldap +# There are two supported backends: `ldap` and `file`. +authentication_backend: + # LDAP backend configuration. + # + # This backend allows Authelia to be scaled to more + # than one instance and therefore is recommended for + # production. + ldap: + # The url of the ldap server + url: ldap://openldap - # The base dn for every entries - base_dn: dc=example,dc=com + # The base dn for every entries + base_dn: dc=example,dc=com - # An additional dn to define the scope to all users - additional_users_dn: ou=users + # An additional dn to define the scope to all users + additional_users_dn: ou=users - # The users filter used to find the user DN - # {0} is a matcher replaced by username. - # 'cn={0}' by default. - users_filter: cn={0} + # The users filter used to find the user DN + # {0} is a matcher replaced by username. + # 'cn={0}' by default. + users_filter: cn={0} - # An additional dn to define the scope of groups - additional_groups_dn: ou=groups + # An additional dn to define the scope of groups + additional_groups_dn: ou=groups - # The groups filter used for retrieving groups of a given user. - # {0} is a matcher replaced by username. - # {dn} is a matcher replaced by user DN. - # 'member={dn}' by default. - groups_filter: (&(member={dn})(objectclass=groupOfNames)) + # The groups filter used for retrieving groups of a given user. + # {0} is a matcher replaced by username. + # {dn} is a matcher replaced by user DN. + # 'member={dn}' by default. + groups_filter: (&(member={dn})(objectclass=groupOfNames)) - # The attribute holding the name of the group - group_name_attribute: cn + # The attribute holding the name of the group + group_name_attribute: cn - # The attribute holding the mail address of the user - mail_attribute: mail + # The attribute holding the mail address of the user + mail_attribute: mail - # The username and password of the admin user. - user: cn=admin,dc=example,dc=com - password: password + # The username and password of the admin user. + user: cn=admin,dc=example,dc=com + password: password + + # File backend configuration. + # + # With this backend, the users database is stored in a file + # which is updated when users reset their passwords. + # Therefore, this backend is meant to be used in a dev environment + # and not in production since it prevents Authelia to be scaled to + # more than one instance. + # + ## file: + ## path: ./users_database.yml # Authentication methods @@ -87,6 +105,7 @@ authentication_methods: 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 @@ -217,8 +236,8 @@ regulation: # You must use only an available configuration: local, mongo storage: # The directory where the DB files will be saved - # local: - # path: /var/lib/authelia/store + ## local: + ## path: /var/lib/authelia/store # Settings to connect to mongo server mongo: @@ -232,16 +251,16 @@ storage: # Use only an available configuration: filesystem, gmail notifier: # For testing purpose, notifications can be sent in a file - # filesystem: - # filename: /tmp/authelia/notification.txt + ## filesystem: + ## 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/ - # email: - # username: user@example.com - # password: yourpassword - # sender: admin@example.com - # service: gmail + ## email: + ## username: user@example.com + ## password: yourpassword + ## sender: admin@example.com + ## service: gmail # Use a SMTP server for sending notifications smtp: diff --git a/docker-compose.minimal.yml b/docker-compose.minimal.yml index fc5a9d5fb..bed34efc2 100644 --- a/docker-compose.minimal.yml +++ b/docker-compose.minimal.yml @@ -5,8 +5,9 @@ services: restart: always volumes: - ./config.minimal.yml:/etc/authelia/config.yml:ro + - ./users_database.test.yml:/etc/authelia/users_database.yml:rw - /tmp/authelia:/tmp/authelia environment: - NODE_TLS_REJECT_UNAUTHORIZED=0 networks: - - example-network \ No newline at end of file + - example-network diff --git a/server/src/lib/Server.spec.ts b/server/src/lib/Server.spec.ts index e3a00e1fe..365163254 100644 --- a/server/src/lib/Server.spec.ts +++ b/server/src/lib/Server.spec.ts @@ -42,11 +42,13 @@ describe("Server", function () { domain: "example.com", secret: "secret" }, - ldap: { - url: "http://ldap", - user: "user", - password: "password", - base_dn: "dc=example,dc=com" + authentication_backend: { + ldap: { + url: "http://ldap", + user: "user", + password: "password", + base_dn: "dc=example,dc=com" + }, }, notifier: { email: { diff --git a/server/src/lib/Server.ts b/server/src/lib/Server.ts index cffb2d9bb..ada66f09e 100644 --- a/server/src/lib/Server.ts +++ b/server/src/lib/Server.ts @@ -35,7 +35,10 @@ export default class Server { const displayableConfiguration: Configuration = clone(configuration); const STARS = "*****"; - displayableConfiguration.ldap.password = STARS; + if (displayableConfiguration.authentication_backend.ldap) { + displayableConfiguration.authentication_backend.ldap.password = STARS; + } + displayableConfiguration.session.secret = STARS; if (displayableConfiguration.notifier && displayableConfiguration.notifier.email) displayableConfiguration.notifier.email.password = STARS; diff --git a/server/src/lib/ServerVariables.ts b/server/src/lib/ServerVariables.ts index e78b48a96..6b3b89e20 100644 --- a/server/src/lib/ServerVariables.ts +++ b/server/src/lib/ServerVariables.ts @@ -6,7 +6,7 @@ import { INotifier } from "./notifiers/INotifier"; import { IRegulator } from "./regulation/IRegulator"; import { Configuration } from "./configuration/schema/Configuration"; import { IAccessController } from "./access_control/IAccessController"; -import { IUsersDatabase } from "./ldap/IUsersDatabase"; +import { IUsersDatabase } from "./authentication/backends/IUsersDatabase"; export interface ServerVariables { logger: IRequestLogger; diff --git a/server/src/lib/ServerVariablesInitializer.ts b/server/src/lib/ServerVariablesInitializer.ts index 432352e82..ad332775c 100644 --- a/server/src/lib/ServerVariablesInitializer.ts +++ b/server/src/lib/ServerVariablesInitializer.ts @@ -11,8 +11,8 @@ import { TotpHandler } from "./authentication/totp/TotpHandler"; import { ITotpHandler } from "./authentication/totp/ITotpHandler"; import { NotifierFactory } from "./notifiers/NotifierFactory"; import { MailSenderBuilder } from "./notifiers/MailSenderBuilder"; -import { LdapUsersDatabase } from "./ldap/LdapUsersDatabase"; -import { ConnectorFactory } from "./ldap/connector/ConnectorFactory"; +import { LdapUsersDatabase } from "./authentication/backends/ldap/LdapUsersDatabase"; +import { ConnectorFactory } from "./authentication/backends/ldap/connector/ConnectorFactory"; import { IUserDataStore } from "./storage/IUserDataStore"; import { UserDataStore } from "./storage/UserDataStore"; @@ -32,7 +32,9 @@ import { ServerVariables } from "./ServerVariables"; import { MethodCalculator } from "./authentication/MethodCalculator"; import { MongoClient } from "./connectors/mongo/MongoClient"; import { IGlobalLogger } from "./logging/IGlobalLogger"; -import { SessionFactory } from "./ldap/SessionFactory"; +import { SessionFactory } from "./authentication/backends/ldap/SessionFactory"; +import { IUsersDatabase } from "./authentication/backends/IUsersDatabase"; +import { FileUsersDatabase } from "./authentication/backends/file/FileUsersDatabase"; class UserDataStoreFactory { static create(config: Configuration.Configuration, globalLogger: IGlobalLogger): BluebirdPromise { @@ -58,24 +60,44 @@ class UserDataStoreFactory { } export class ServerVariablesInitializer { + static createUsersDatabase( + config: Configuration.Configuration, + deps: GlobalDependencies) + : IUsersDatabase { + + if (config.authentication_backend.ldap) { + const ldapConfig = config.authentication_backend.ldap; + return new LdapUsersDatabase( + new SessionFactory( + ldapConfig, + new ConnectorFactory(ldapConfig, deps.ldapjs), + deps.winston + ), + ldapConfig + ); + } + else if (config.authentication_backend.file) { + return new FileUsersDatabase(config.authentication_backend.file); + } + } + static initialize( config: Configuration.Configuration, globalLogger: IGlobalLogger, requestLogger: IRequestLogger, - deps: GlobalDependencies): BluebirdPromise { + deps: GlobalDependencies) + : BluebirdPromise { - const mailSenderBuilder = new MailSenderBuilder(Nodemailer); - const notifier = NotifierFactory.build(config.notifier, mailSenderBuilder); - const ldapUsersDatabase = new LdapUsersDatabase( - new SessionFactory( - config.ldap, - new ConnectorFactory(config.ldap, deps.ldapjs), - deps.winston - ), - config.ldap - ); - const accessController = new AccessController(config.access_control, deps.winston); - const totpHandler = new TotpHandler(deps.speakeasy); + const mailSenderBuilder = + 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 usersDatabase = this.createUsersDatabase( + config, deps); return UserDataStoreFactory.create(config, globalLogger) .then(function (userDataStore: UserDataStore) { @@ -85,7 +107,7 @@ export class ServerVariablesInitializer { const variables: ServerVariables = { accessController: accessController, config: config, - usersDatabase: ldapUsersDatabase, + usersDatabase: usersDatabase, logger: requestLogger, notifier: notifier, regulator: regulator, diff --git a/server/src/lib/ServerVariablesMockBuilder.spec.ts b/server/src/lib/ServerVariablesMockBuilder.spec.ts index a0e41797e..25014e218 100644 --- a/server/src/lib/ServerVariablesMockBuilder.spec.ts +++ b/server/src/lib/ServerVariablesMockBuilder.spec.ts @@ -1,7 +1,7 @@ import { ServerVariables } from "./ServerVariables"; import { Configuration } from "./configuration/schema/Configuration"; -import { UsersDatabaseStub } from "./ldap/UsersDatabaseStub.spec"; +import { IUsersDatabaseStub } from "./authentication/backends/IUsersDatabaseStub.spec"; import { AccessControllerStub } from "./access_control/AccessControllerStub.spec"; import { RequestLoggerStub } from "./logging/RequestLoggerStub.spec"; import { NotifierStub } from "./notifiers/NotifierStub.spec"; @@ -13,7 +13,7 @@ import { U2fHandlerStub } from "./authentication/u2f/U2fHandlerStub.spec"; export interface ServerVariablesMock { accessController: AccessControllerStub; config: Configuration; - usersDatabase: UsersDatabaseStub; + usersDatabase: IUsersDatabaseStub; logger: RequestLoggerStub; notifier: NotifierStub; regulator: RegulatorStub; @@ -34,17 +34,19 @@ export class ServerVariablesMockBuilder { totp: { issuer: "authelia.com" }, - ldap: { - url: "ldap://ldap", - base_dn: "dc=example,dc=com", - user: "user", - password: "password", - mail_attribute: "mail", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - users_filter: "cn={0}", - groups_filter: "member={dn}", - group_name_attribute: "cn" + authentication_backend: { + ldap: { + url: "ldap://ldap", + base_dn: "dc=example,dc=com", + user: "user", + password: "password", + mail_attribute: "mail", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + users_filter: "cn={0}", + groups_filter: "member={dn}", + group_name_attribute: "cn" + }, }, logs_level: "debug", notifier: {}, @@ -60,7 +62,7 @@ export class ServerVariablesMockBuilder { }, storage: {} }, - usersDatabase: new UsersDatabaseStub(), + usersDatabase: new IUsersDatabaseStub(), logger: new RequestLoggerStub(enableLogging), notifier: new NotifierStub(), regulator: new RegulatorStub(), diff --git a/server/src/lib/authentication/backends/GroupsAndEmails.ts b/server/src/lib/authentication/backends/GroupsAndEmails.ts new file mode 100644 index 000000000..3434ba66f --- /dev/null +++ b/server/src/lib/authentication/backends/GroupsAndEmails.ts @@ -0,0 +1,5 @@ + +export interface GroupsAndEmails { + groups: string[]; + emails: string[]; +} diff --git a/server/src/lib/ldap/IUsersDatabase.ts b/server/src/lib/authentication/backends/IUsersDatabase.ts similarity index 85% rename from server/src/lib/ldap/IUsersDatabase.ts rename to server/src/lib/authentication/backends/IUsersDatabase.ts index 7e814b7ec..d7fa13b7a 100644 --- a/server/src/lib/ldap/IUsersDatabase.ts +++ b/server/src/lib/authentication/backends/IUsersDatabase.ts @@ -1,5 +1,6 @@ import Bluebird = require("bluebird"); -import { GroupsAndEmails } from "./ISession"; + +import { GroupsAndEmails } from "./GroupsAndEmails"; export interface IUsersDatabase { checkUserPassword(username: string, password: string): Bluebird; diff --git a/server/src/lib/ldap/UsersDatabaseStub.spec.ts b/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts similarity index 89% rename from server/src/lib/ldap/UsersDatabaseStub.spec.ts rename to server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts index f92aeb6a4..19341a5dd 100644 --- a/server/src/lib/ldap/UsersDatabaseStub.spec.ts +++ b/server/src/lib/authentication/backends/IUsersDatabaseStub.spec.ts @@ -2,9 +2,9 @@ import Bluebird = require("bluebird"); import Sinon = require("sinon"); import { IUsersDatabase } from "./IUsersDatabase"; -import { GroupsAndEmails } from "./ISession"; +import { GroupsAndEmails } from "./GroupsAndEmails"; -export class UsersDatabaseStub implements IUsersDatabase { +export class IUsersDatabaseStub implements IUsersDatabase { checkUserPasswordStub: Sinon.SinonStub; getEmailsStub: Sinon.SinonStub; getGroupsStub: Sinon.SinonStub; diff --git a/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts b/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts new file mode 100644 index 000000000..a258a78f7 --- /dev/null +++ b/server/src/lib/authentication/backends/file/FileUsersDatabase.spec.ts @@ -0,0 +1,224 @@ +import Assert = require("assert"); +import Bluebird = require("bluebird"); +import Fs = require("fs"); +import Sinon = require("sinon"); +import Tmp = require("tmp"); + +import { FileUsersDatabase } from "./FileUsersDatabase"; +import { FileUsersDatabaseConfiguration } from "../../../configuration/schema/FileUsersDatabaseConfiguration"; +import { HashGenerator } from "../../../utils/HashGenerator"; + +const GOOD_DATABASE = ` +users: + john: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev + + harry: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + emails: harry.potter@authelia.com + groups: [] +`; + +const BAD_HASH = ` +users: + john: + password: "{CRYPT}$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev +`; + +const NO_PASSWORD_DATABASE = ` +users: + john: + email: john.doe@authelia.com + groups: + - admins + - dev +`; + +const NO_EMAIL_DATABASE = ` +users: + john: + password: "{CRYPT}$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + groups: + - admins + - dev +`; + +const SINGLE_USER_DATABASE = ` +users: + john: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev +` + +function createTmpFileFrom(yaml: string) { + const tmpFileAsync = Bluebird.promisify(Tmp.file); + return tmpFileAsync() + .then((path: string) => { + Fs.writeFileSync(path, yaml, "utf-8"); + return Bluebird.resolve(path); + }); +} + +describe("authentication/backends/file/FileUsersDatabase", function() { + let configuration: FileUsersDatabaseConfiguration; + + describe("checkUserPassword", () => { + describe("good config", () => { + beforeEach(() => { + return createTmpFileFrom(GOOD_DATABASE) + .then((path: string) => configuration = { + path: path + }); + }); + + it("should succeed", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.checkUserPassword("john", "password") + .then((groupsAndEmails) => { + Assert.deepEqual(groupsAndEmails.groups, ["admins", "dev"]); + Assert.deepEqual(groupsAndEmails.emails, ["john.doe@authelia.com"]); + }); + }); + + it("should fail when password is wrong", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.checkUserPassword("john", "bad_password") + .then(() => Bluebird.reject(new Error("should not be here."))) + .catch((err) => { + return Bluebird.resolve(); + }); + }); + + it("should fail when user does not exist", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.checkUserPassword("no_user", "password") + .then(() => Bluebird.reject(new Error("should not be here."))) + .catch((err) => { + return Bluebird.resolve(); + }); + }); + }); + + describe("bad hash", () => { + beforeEach(() => { + return createTmpFileFrom(GOOD_DATABASE) + .then((path: string) => configuration = { + path: path + }); + }); + + it("should fail when hash is wrong", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.checkUserPassword("john", "password") + .then(() => Bluebird.reject(new Error("should not be here."))) + .catch((err) => { + return Bluebird.resolve(); + }); + }); + }); + + describe("no password", () => { + beforeEach(() => { + return createTmpFileFrom(NO_PASSWORD_DATABASE) + .then((path: string) => configuration = { + path: path + }); + }); + + it("should fail", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.checkUserPassword("john", "password") + .then(() => Bluebird.reject(new Error("should not be here."))) + .catch((err) => { + return Bluebird.resolve(); + }); + }); + }); + }); + + describe("getEmails", () => { + describe("good config", () => { + beforeEach(() => { + return createTmpFileFrom(GOOD_DATABASE) + .then((path: string) => configuration = { + path: path + }); + }); + + it("should succeed", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.getEmails("john") + .then((emails) => { + Assert.deepEqual(emails, ["john.doe@authelia.com"]); + }); + }); + + it("should fail when user does not exist", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.getEmails("no_user") + .then(() => Bluebird.reject(new Error("should not be here."))) + .catch((err) => { + return Bluebird.resolve(); + }); + }); + }); + + describe("no email provided", () => { + beforeEach(() => { + return createTmpFileFrom(NO_EMAIL_DATABASE) + .then((path: string) => configuration = { + path: path + }); + }); + + it("should fail", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.getEmails("john") + .then(() => Bluebird.reject(new Error("should not be here."))) + .catch((err) => { + return Bluebird.resolve(); + }); + }); + }); + }); + + describe("updatePassword", () => { + beforeEach(() => { + return createTmpFileFrom(SINGLE_USER_DATABASE) + .then((path: string) => configuration = { + path: path + }); + }); + + it("should succeed", () => { + const usersDatabase = new FileUsersDatabase(configuration); + const NEW_HASH = "{CRYPT}$6$rounds=500000$Qw6MhgADvLyYMEq9$ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const stub = Sinon.stub(HashGenerator, "ssha512").returns(Bluebird.resolve(NEW_HASH)); + return usersDatabase.updatePassword("john", "mypassword") + .then(() => { + const content = Fs.readFileSync(configuration.path, "utf-8"); + const matches = content.match(/password: '(.+)'/); + Assert.equal(matches[1], NEW_HASH); + }) + .finally(() => stub.restore()); + }); + + it("should fail when user does not exist", () => { + const usersDatabase = new FileUsersDatabase(configuration); + return usersDatabase.updatePassword("bad_user", "mypassword") + .then(() => Bluebird.reject(new Error("should not be here"))) + .catch(() => Bluebird.resolve()); + }); + }); +}); \ No newline at end of file diff --git a/server/src/lib/authentication/backends/file/FileUsersDatabase.ts b/server/src/lib/authentication/backends/file/FileUsersDatabase.ts new file mode 100644 index 000000000..d34dde21e --- /dev/null +++ b/server/src/lib/authentication/backends/file/FileUsersDatabase.ts @@ -0,0 +1,182 @@ +import Bluebird = require("bluebird"); +import Fs = require("fs"); +import Yaml = require("yamljs"); + +import { FileUsersDatabaseConfiguration } + from "../../../configuration/schema/FileUsersDatabaseConfiguration"; +import { GroupsAndEmails } from "../GroupsAndEmails"; +import { IUsersDatabase } from "../IUsersDatabase"; +import { HashGenerator } from "../../../utils/HashGenerator"; +import { ReadWriteQueue } from "./ReadWriteQueue"; + +const loadAsync = Bluebird.promisify(Yaml.load); + +export class FileUsersDatabase implements IUsersDatabase { + private configuration: FileUsersDatabaseConfiguration; + private queue: ReadWriteQueue; + + constructor(configuration: FileUsersDatabaseConfiguration) { + this.configuration = configuration; + this.queue = new ReadWriteQueue(this.configuration.path); + } + + /** + * Read database from file. + * It enqueues the read task so that it is scheduled + * between other reads and writes. + */ + private readDatabase(): Bluebird { + return new Bluebird((resolve, reject) => { + this.queue.read((err: Error, data: string) => { + if (err) { + reject(err); + return; + } + resolve(data); + this.queue.next(); + }); + }) + .then((content) => { + const database = Yaml.parse(content); + if (!database) { + return Bluebird.reject(new Error("Unable to parse YAML file.")); + } + return Bluebird.resolve(database); + }); + } + + /** + * Checks the user exists in the database. + */ + private checkUserExists( + database: any, + username: string) + : Bluebird { + if (!(username in database.users)) { + return Bluebird.reject( + new Error(`User ${username} does not exist in database.`)); + } + return Bluebird.resolve(); + } + + /** + * Check the password of a given user. + */ + private checkPassword( + database: any, + username: string, + password: string) + : Bluebird { + const storedHash: string = database.users[username].password; + const matches = storedHash.match(/rounds=([0-9]+)\$([a-zA-z0-9]+)\$/); + if (!(matches && matches.length == 3)) { + return Bluebird.reject(new Error("Unable to detect the hash salt and rounds. " + + "Make sure the password is hashed with SSHA512.")); + } + + const rounds: number = parseInt(matches[1]); + const salt = matches[2]; + + return HashGenerator.ssha512(password, rounds, salt) + .then((hash: string) => { + if (hash !== storedHash) { + return Bluebird.reject(new Error("Wrong username/password.")); + } + return Bluebird.resolve(); + }); + } + + /** + * Retrieve email addresses of a given user. + */ + private retrieveEmails( + database: any, + username: string) + : Bluebird { + if (!("email" in database.users[username])) { + return Bluebird.reject( + new Error(`User ${username} has no email address.`)); + } + return Bluebird.resolve( + [database.users[username].email]); + } + + private retrieveGroups( + database: any, + username: string) + : Bluebird { + if (!("groups" in database.users[username])) { + return Bluebird.resolve([]); + } + return Bluebird.resolve( + database.users[username].groups); + } + + private replacePassword( + database: any, + username: string, + newPassword: string) + : Bluebird { + const that = this; + return HashGenerator.ssha512(newPassword) + .then((hash) => { + database.users[username].password = hash; + const str = Yaml.stringify(database, 4, 2); + return Bluebird.resolve(str); + }) + .then((content: string) => { + return new Bluebird((resolve, reject) => { + that.queue.write(content, (err) => { + if (err) { + return reject(err); + } + resolve(); + that.queue.next(); + }); + }); + }); + } + + checkUserPassword( + username: string, + password: string) + : Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.checkPassword(database, username, password)) + .then(() => { + return Bluebird.join( + this.retrieveEmails(database, username), + this.retrieveGroups(database, username) + ).spread((emails: string[], groups: string[]) => { + return { emails: emails, groups: groups }; + }); + }); + }); + } + + getEmails(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveEmails(database, username)); + }); + } + + getGroups(username: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.retrieveGroups(database, username)); + }); + } + + updatePassword(username: string, newPassword: string): Bluebird { + return this.readDatabase() + .then((database) => { + return this.checkUserExists(database, username) + .then(() => this.replacePassword(database, username, newPassword)); + }); + } +} \ No newline at end of file diff --git a/server/src/lib/authentication/backends/file/ReadWriteQueue.ts b/server/src/lib/authentication/backends/file/ReadWriteQueue.ts new file mode 100644 index 000000000..957ddaec5 --- /dev/null +++ b/server/src/lib/authentication/backends/file/ReadWriteQueue.ts @@ -0,0 +1,60 @@ +import Fs = require("fs"); + +type Callback = (err: Error, data?: string) => void; +type ContentAndCallback = [string, Callback] | [string, string, Callback]; + +/** + * WriteQueue is a queue synchronizing writes to a file. + * + * Example of use: + * + * queue.add(mycontent, (err) => { + * // do whatever you want here. + * queue.next(); + * }) + */ +export class ReadWriteQueue { + private filePath: string; + private queue: ContentAndCallback[]; + + constructor (filePath: string) { + this.queue = []; + this.filePath = filePath; + } + + next () { + if (this.queue.length === 0) + return; + + const task = this.queue[0]; + + if (task[0] == "write") { + Fs.writeFile(this.filePath, task[1], "utf-8", (err) => { + this.queue.shift(); + const cb = task[2] as Callback; + cb(err); + }); + } + else if (task[0] == "read") { + Fs.readFile(this.filePath, { encoding: "utf-8"} , (err, data) => { + this.queue.shift(); + const cb = task[1] as Callback; + cb(err, data); + }); + } + } + + write (content: string, cb: Callback) { + this.queue.push(["write", content, cb]); + if (this.queue.length === 1) { + this.next(); + } + } + + read (cb: Callback) { + this.queue.push(["read", cb]); + if (this.queue.length === 1) { + this.next(); + } + } +} \ No newline at end of file diff --git a/server/src/lib/ldap/ISession.ts b/server/src/lib/authentication/backends/ldap/ISession.ts similarity index 83% rename from server/src/lib/ldap/ISession.ts rename to server/src/lib/authentication/backends/ldap/ISession.ts index 2cb757268..da2c74433 100644 --- a/server/src/lib/ldap/ISession.ts +++ b/server/src/lib/authentication/backends/ldap/ISession.ts @@ -1,11 +1,6 @@ import BluebirdPromise = require("bluebird"); -export interface GroupsAndEmails { - groups: string[]; - emails: string[]; -} - export interface ISession { open(): BluebirdPromise; close(): BluebirdPromise; diff --git a/server/src/lib/ldap/ISessionFactory.ts b/server/src/lib/authentication/backends/ldap/ISessionFactory.ts similarity index 100% rename from server/src/lib/ldap/ISessionFactory.ts rename to server/src/lib/authentication/backends/ldap/ISessionFactory.ts diff --git a/server/src/lib/ldap/LdapUsersDatabase.spec.ts b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts similarity index 100% rename from server/src/lib/ldap/LdapUsersDatabase.spec.ts rename to server/src/lib/authentication/backends/ldap/LdapUsersDatabase.spec.ts diff --git a/server/src/lib/ldap/LdapUsersDatabase.ts b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts similarity index 91% rename from server/src/lib/ldap/LdapUsersDatabase.ts rename to server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts index 5631018ce..edda62ec6 100644 --- a/server/src/lib/ldap/LdapUsersDatabase.ts +++ b/server/src/lib/authentication/backends/ldap/LdapUsersDatabase.ts @@ -1,9 +1,10 @@ import Bluebird = require("bluebird"); -import { IUsersDatabase } from "./IUsersDatabase"; +import { IUsersDatabase } from "../IUsersDatabase"; import { ISessionFactory } from "./ISessionFactory"; -import { LdapConfiguration } from "../configuration/schema/LdapConfiguration"; -import { ISession, GroupsAndEmails } from "./ISession"; -import Exceptions = require("../Exceptions"); +import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration"; +import { ISession } from "./ISession"; +import { GroupsAndEmails } from "../GroupsAndEmails"; +import Exceptions = require("../../../Exceptions"); type SessionCallback = (session: ISession) => Bluebird; diff --git a/server/src/lib/ldap/SafeSession.spec.ts b/server/src/lib/authentication/backends/ldap/SafeSession.spec.ts similarity index 100% rename from server/src/lib/ldap/SafeSession.spec.ts rename to server/src/lib/authentication/backends/ldap/SafeSession.spec.ts diff --git a/server/src/lib/ldap/SafeSession.ts b/server/src/lib/authentication/backends/ldap/SafeSession.ts similarity index 96% rename from server/src/lib/ldap/SafeSession.ts rename to server/src/lib/authentication/backends/ldap/SafeSession.ts index 4c9dc01dd..572209066 100644 --- a/server/src/lib/ldap/SafeSession.ts +++ b/server/src/lib/authentication/backends/ldap/SafeSession.ts @@ -1,5 +1,5 @@ import BluebirdPromise = require("bluebird"); -import { ISession, GroupsAndEmails } from "./ISession"; +import { ISession } from "./ISession"; import { Sanitizer } from "./Sanitizer"; const SPECIAL_CHAR_USED_MESSAGE = "Special character used in LDAP query."; diff --git a/server/src/lib/ldap/Sanitizer.spec.ts b/server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts similarity index 100% rename from server/src/lib/ldap/Sanitizer.spec.ts rename to server/src/lib/authentication/backends/ldap/Sanitizer.spec.ts diff --git a/server/src/lib/ldap/Sanitizer.ts b/server/src/lib/authentication/backends/ldap/Sanitizer.ts similarity index 100% rename from server/src/lib/ldap/Sanitizer.ts rename to server/src/lib/authentication/backends/ldap/Sanitizer.ts diff --git a/server/src/lib/ldap/Session.spec.ts b/server/src/lib/authentication/backends/ldap/Session.spec.ts similarity index 97% rename from server/src/lib/ldap/Session.spec.ts rename to server/src/lib/authentication/backends/ldap/Session.spec.ts index 164caae27..d55f6a805 100644 --- a/server/src/lib/ldap/Session.spec.ts +++ b/server/src/lib/authentication/backends/ldap/Session.spec.ts @@ -1,5 +1,5 @@ -import { LdapConfiguration } from "../configuration/schema/LdapConfiguration"; +import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration"; import { Session } from "./Session"; import { ConnectorFactoryStub } from "./connector/ConnectorFactoryStub.spec"; import { ConnectorStub } from "./connector/ConnectorStub.spec"; diff --git a/server/src/lib/ldap/Session.ts b/server/src/lib/authentication/backends/ldap/Session.ts similarity index 94% rename from server/src/lib/ldap/Session.ts rename to server/src/lib/authentication/backends/ldap/Session.ts index e5ba1804e..e0284b3c4 100644 --- a/server/src/lib/ldap/Session.ts +++ b/server/src/lib/authentication/backends/ldap/Session.ts @@ -1,11 +1,11 @@ import BluebirdPromise = require("bluebird"); -import exceptions = require("../Exceptions"); +import exceptions = require("../../../Exceptions"); import { EventEmitter } from "events"; -import { ISession, GroupsAndEmails } from "./ISession"; -import { LdapConfiguration } from "../configuration/schema/LdapConfiguration"; -import { Winston } from "../../../types/Dependencies"; +import { ISession } from "./ISession"; +import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration"; +import { Winston } from "../../../../../types/Dependencies"; import Util = require("util"); -import { HashGenerator } from "../utils/HashGenerator"; +import { HashGenerator } from "../../../utils/HashGenerator"; import { IConnector } from "./connector/IConnector"; export class Session implements ISession { diff --git a/server/src/lib/ldap/SessionFactory.ts b/server/src/lib/authentication/backends/ldap/SessionFactory.ts similarity index 92% rename from server/src/lib/ldap/SessionFactory.ts rename to server/src/lib/authentication/backends/ldap/SessionFactory.ts index 2a71ac8cc..0b6c4bff3 100644 --- a/server/src/lib/ldap/SessionFactory.ts +++ b/server/src/lib/authentication/backends/ldap/SessionFactory.ts @@ -4,7 +4,7 @@ import Winston = require("winston"); import { IConnectorFactory } from "./connector/IConnectorFactory"; import { ISessionFactory } from "./ISessionFactory"; import { ISession } from "./ISession"; -import { LdapConfiguration } from "../configuration/schema/LdapConfiguration"; +import { LdapConfiguration } from "../../../configuration/schema/LdapConfiguration"; import { Session } from "./Session"; import { SafeSession } from "./SafeSession"; diff --git a/server/src/lib/ldap/SessionFactoryStub.spec.ts b/server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts similarity index 100% rename from server/src/lib/ldap/SessionFactoryStub.spec.ts rename to server/src/lib/authentication/backends/ldap/SessionFactoryStub.spec.ts diff --git a/server/src/lib/ldap/SessionStub.spec.ts b/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts similarity index 95% rename from server/src/lib/ldap/SessionStub.spec.ts rename to server/src/lib/authentication/backends/ldap/SessionStub.spec.ts index 383eb1c01..5faf2ba18 100644 --- a/server/src/lib/ldap/SessionStub.spec.ts +++ b/server/src/lib/authentication/backends/ldap/SessionStub.spec.ts @@ -1,7 +1,7 @@ import Bluebird = require("bluebird"); import Sinon = require("sinon"); -import { ISession, GroupsAndEmails } from "./ISession"; +import { ISession } from "./ISession"; export class SessionStub implements ISession { openStub: Sinon.SinonStub; diff --git a/server/src/lib/ldap/connector/Connector.ts b/server/src/lib/authentication/backends/ldap/connector/Connector.ts similarity index 97% rename from server/src/lib/ldap/connector/Connector.ts rename to server/src/lib/authentication/backends/ldap/connector/Connector.ts index 866c2b5d2..2542ea7f9 100644 --- a/server/src/lib/ldap/connector/Connector.ts +++ b/server/src/lib/authentication/backends/ldap/connector/Connector.ts @@ -2,7 +2,7 @@ import LdapJs = require("ldapjs"); import EventEmitter = require("events"); import Bluebird = require("bluebird"); import { IConnector } from "./IConnector"; -import Exceptions = require("../../Exceptions"); +import Exceptions = require("../../../../Exceptions"); interface SearchEntry { object: any; diff --git a/server/src/lib/ldap/connector/ConnectorFactory.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts similarity index 83% rename from server/src/lib/ldap/connector/ConnectorFactory.ts rename to server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts index 78380aa10..61fef07a4 100644 --- a/server/src/lib/ldap/connector/ConnectorFactory.ts +++ b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactory.ts @@ -1,6 +1,6 @@ import { IConnector } from "./IConnector"; import { Connector } from "./Connector"; -import { LdapConfiguration } from "../../configuration/schema/LdapConfiguration"; +import { LdapConfiguration } from "../../../../configuration/schema/LdapConfiguration"; import { Ldapjs } from "Dependencies"; export class ConnectorFactory { diff --git a/server/src/lib/ldap/connector/ConnectorFactoryStub.spec.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts similarity index 99% rename from server/src/lib/ldap/connector/ConnectorFactoryStub.spec.ts rename to server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts index 7938a755a..d11fa6386 100644 --- a/server/src/lib/ldap/connector/ConnectorFactoryStub.spec.ts +++ b/server/src/lib/authentication/backends/ldap/connector/ConnectorFactoryStub.spec.ts @@ -1,5 +1,6 @@ -import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); + import { IConnectorFactory } from "./IConnectorFactory"; import { IConnector } from "./IConnector"; diff --git a/server/src/lib/ldap/connector/ConnectorStub.spec.ts b/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts similarity index 99% rename from server/src/lib/ldap/connector/ConnectorStub.spec.ts rename to server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts index fc902b8c2..0b78225bf 100644 --- a/server/src/lib/ldap/connector/ConnectorStub.spec.ts +++ b/server/src/lib/authentication/backends/ldap/connector/ConnectorStub.spec.ts @@ -1,5 +1,6 @@ -import Sinon = require("sinon"); import BluebirdPromise = require("bluebird"); +import Sinon = require("sinon"); + import { IConnector } from "./IConnector"; export class ConnectorStub implements IConnector { diff --git a/server/src/lib/ldap/connector/IConnector.ts b/server/src/lib/authentication/backends/ldap/connector/IConnector.ts similarity index 99% rename from server/src/lib/ldap/connector/IConnector.ts rename to server/src/lib/authentication/backends/ldap/connector/IConnector.ts index 76bd35b6a..1e63ab193 100644 --- a/server/src/lib/ldap/connector/IConnector.ts +++ b/server/src/lib/authentication/backends/ldap/connector/IConnector.ts @@ -1,4 +1,3 @@ - import Bluebird = require("bluebird"); import EventEmitter = require("events"); diff --git a/server/src/lib/ldap/connector/IConnectorFactory.ts b/server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts similarity index 100% rename from server/src/lib/ldap/connector/IConnectorFactory.ts rename to server/src/lib/authentication/backends/ldap/connector/IConnectorFactory.ts diff --git a/server/src/lib/configuration/ConfigurationParser.spec.ts b/server/src/lib/configuration/ConfigurationParser.spec.ts index bb304386d..ba16e1640 100644 --- a/server/src/lib/configuration/ConfigurationParser.spec.ts +++ b/server/src/lib/configuration/ConfigurationParser.spec.ts @@ -7,13 +7,15 @@ describe("configuration/ConfigurationParser", function () { function buildYamlConfig(): Configuration { const yaml_config: Configuration = { port: 8080, - ldap: { - url: "http://ldap", - base_dn: "dc=example,dc=com", - additional_users_dn: "ou=users", - additional_groups_dn: "ou=groups", - user: "user", - password: "pass" + authentication_backend: { + ldap: { + url: "http://ldap", + base_dn: "dc=example,dc=com", + additional_users_dn: "ou=users", + additional_groups_dn: "ou=groups", + user: "user", + password: "pass" + }, }, session: { domain: "example.com", diff --git a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts index a03507d64..3f7eb592b 100644 --- a/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts +++ b/server/src/lib/configuration/SessionConfigurationBuilder.spec.ts @@ -19,17 +19,19 @@ describe("configuration/SessionConfigurationBuilder", function () { totp: { issuer: "authelia.com" }, - ldap: { - url: "ldap://ldap", - user: "user", - base_dn: "dc=example,dc=com", - password: "password", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + base_dn: "dc=example,dc=com", + password: "password", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, }, logs_level: "debug", notifier: { @@ -100,17 +102,19 @@ describe("configuration/SessionConfigurationBuilder", function () { totp: { issuer: "authelia.com" }, - ldap: { - url: "ldap://ldap", - user: "user", - password: "password", - base_dn: "dc=example,dc=com", - additional_groups_dn: "ou=groups", - additional_users_dn: "ou=users", - group_name_attribute: "", - groups_filter: "", - mail_attribute: "", - users_filter: "" + authentication_backend: { + ldap: { + url: "ldap://ldap", + user: "user", + password: "password", + base_dn: "dc=example,dc=com", + additional_groups_dn: "ou=groups", + additional_users_dn: "ou=users", + group_name_attribute: "", + groups_filter: "", + mail_attribute: "", + users_filter: "" + }, }, logs_level: "debug", notifier: { diff --git a/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts new file mode 100644 index 000000000..3ca86381c --- /dev/null +++ b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.spec.ts @@ -0,0 +1,11 @@ +import { AuthenticationBackendConfiguration, complete } from "./AuthenticationBackendConfiguration"; +import Assert = require("assert"); + +describe("configuration/schema/AuthenticationBackendConfiguration", function() { + it("should ensure there is at least one key", function() { + const configuration: AuthenticationBackendConfiguration = {} as any; + const [newConfiguration, error] = complete(configuration); + + Assert.equal(error, "Authentication backend must have one of the following keys:`ldap` or `file`"); + }); +}); \ No newline at end of file diff --git a/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts new file mode 100644 index 000000000..7f77f894f --- /dev/null +++ b/server/src/lib/configuration/schema/AuthenticationBackendConfiguration.ts @@ -0,0 +1,25 @@ +import { LdapConfiguration } from "./LdapConfiguration"; +import { FileUsersDatabaseConfiguration } from "./FileUsersDatabaseConfiguration"; + +export interface AuthenticationBackendConfiguration { + ldap?: LdapConfiguration; + file?: FileUsersDatabaseConfiguration; +} + +export function complete( + configuration: AuthenticationBackendConfiguration) + : [AuthenticationBackendConfiguration, string] { + + const newConfiguration: AuthenticationBackendConfiguration = (configuration) + ? JSON.parse(JSON.stringify(configuration)) : {}; + + if (Object.keys(newConfiguration).length != 1) { + return [ + newConfiguration, + "Authentication backend must have one of the following keys:" + + "`ldap` or `file`" + ]; + } + + return [newConfiguration, undefined]; +} \ 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 c1fc5dcb6..7777125e4 100644 --- a/server/src/lib/configuration/schema/Configuration.ts +++ b/server/src/lib/configuration/schema/Configuration.ts @@ -1,6 +1,6 @@ import { ACLConfiguration, complete as AclConfigurationComplete } from "./AclConfiguration"; import { AuthenticationMethodsConfiguration, complete as AuthenticationMethodsConfigurationComplete } from "./AuthenticationMethodsConfiguration"; -import { LdapConfiguration, complete as LdapConfigurationComplete } from "./LdapConfiguration"; +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"; @@ -10,7 +10,7 @@ import { MethodCalculator } from "../../authentication/MethodCalculator"; export interface Configuration { access_control?: ACLConfiguration; - ldap: LdapConfiguration; + authentication_backend: AuthenticationBackendConfiguration; authentication_methods?: AuthenticationMethodsConfiguration; default_redirection_url?: string; logs_level?: string; @@ -30,10 +30,16 @@ export function complete( JSON.stringify(configuration)); const errors: string[] = []; - newConfiguration.access_control = AclConfigurationComplete( - newConfiguration.access_control); - newConfiguration.ldap = LdapConfigurationComplete( - newConfiguration.ldap); + newConfiguration.access_control = + AclConfigurationComplete( + newConfiguration.access_control); + + const [backend, error] = + AuthenticationBackendComplete( + newConfiguration.authentication_backend); + + if (error) errors.push(error); + newConfiguration.authentication_backend = backend; newConfiguration.authentication_methods = AuthenticationMethodsConfigurationComplete( diff --git a/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts b/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts new file mode 100644 index 000000000..d19002ba9 --- /dev/null +++ b/server/src/lib/configuration/schema/FileUsersDatabaseConfiguration.ts @@ -0,0 +1,4 @@ + +export interface FileUsersDatabaseConfiguration { + path: 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 bc3d80536..94136b44a 100644 --- a/server/src/lib/routes/firstfactor/post.ts +++ b/server/src/lib/routes/firstfactor/post.ts @@ -14,7 +14,7 @@ import UserMessages = require("../../../../../shared/UserMessages"); import { MethodCalculator } from "../../authentication/MethodCalculator"; import { ServerVariables } from "../../ServerVariables"; import { AuthenticationSession } from "../../../../types/AuthenticationSession"; -import { GroupsAndEmails } from "../../ldap/ISession"; +import { GroupsAndEmails } from "../../authentication/backends/GroupsAndEmails"; export default function (vars: ServerVariables) { return function (req: express.Request, res: express.Response) 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 667ce35a9..8d6389718 100644 --- a/server/src/lib/routes/password-reset/form/post.spec.ts +++ b/server/src/lib/routes/password-reset/form/post.spec.ts @@ -42,7 +42,7 @@ describe("routes/password-reset/form/post", function () { mocks.userDataStore.produceIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); mocks.userDataStore.consumeIdentityValidationTokenStub.returns(BluebirdPromise.resolve({})); - mocks.config.ldap = { + mocks.config.authentication_backend.ldap = { url: "ldap://ldapjs", mail_attribute: "mail", user: "user", diff --git a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts index ab8ef4f30..42ae92cda 100644 --- a/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts +++ b/server/src/lib/routes/password-reset/identity/PasswordResetHandler.ts @@ -8,7 +8,7 @@ import { IdentityValidable } from "../../../IdentityValidable"; import { PRE_VALIDATION_TEMPLATE } from "../../../IdentityCheckPreValidationTemplate"; import Constants = require("../constants"); import { IRequestLogger } from "../../../logging/IRequestLogger"; -import { IUsersDatabase } from "../../../ldap/IUsersDatabase"; +import { IUsersDatabase } from "../../../authentication/backends/IUsersDatabase"; export const TEMPLATE_NAME = "password-reset-form"; diff --git a/server/src/lib/utils/HashGenerator.spec.ts b/server/src/lib/utils/HashGenerator.spec.ts index 0a94e3292..f19619a65 100644 --- a/server/src/lib/utils/HashGenerator.spec.ts +++ b/server/src/lib/utils/HashGenerator.spec.ts @@ -3,14 +3,14 @@ import { HashGenerator } from "./HashGenerator"; describe("utils/HashGenerator", function () { it("should compute correct ssha512 (password)", function () { - return HashGenerator.ssha512("password", "jgiCMRyGXzoqpxS3") + return HashGenerator.ssha512("password", 500000, "jgiCMRyGXzoqpxS3") .then(function (hash: string) { Assert.equal(hash, "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/"); }); }); it("should compute correct ssha512 (test)", function () { - return HashGenerator.ssha512("test", "abcdefghijklmnop") + return HashGenerator.ssha512("test", 500000, "abcdefghijklmnop") .then(function (hash: string) { Assert.equal(hash, "{CRYPT}$6$rounds=500000$abcdefghijklmnop$sTlNGf0VO/HTQIOXemmaBbV28HUch/qhWOA1/4dsDj6CDQYhUgXbYSPL6gccAsWMr2zD5fFWwhKmPdG.yxphs."); }); diff --git a/server/src/lib/utils/HashGenerator.ts b/server/src/lib/utils/HashGenerator.ts index 410b522f1..e67de32b7 100644 --- a/server/src/lib/utils/HashGenerator.ts +++ b/server/src/lib/utils/HashGenerator.ts @@ -4,8 +4,10 @@ import Util = require("util"); const crypt = require("crypt3"); export class HashGenerator { - static ssha512(password: string, salt?: string): BluebirdPromise { - const rounds = 500000; + static ssha512( + password: string, + rounds: number = 500000, + salt?: string): BluebirdPromise { const saltSize = 16; // $6 means SHA512 const _salt = Util.format("$6$rounds=%d$%s", rounds, diff --git a/test/helpers/click-on-link.ts b/test/helpers/click-on-link.ts new file mode 100644 index 000000000..97f791022 --- /dev/null +++ b/test/helpers/click-on-link.ts @@ -0,0 +1,10 @@ +import SeleniumWebdriver = require("selenium-webdriver"); + +export default function(driver: any, linkText: string) { + return driver.wait( + SeleniumWebdriver.until.elementLocated( + SeleniumWebdriver.By.linkText(linkText)), 5000) + .then(function (el) { + return el.click(); + }); +}; \ No newline at end of file diff --git a/test/helpers/fill-field.ts b/test/helpers/fill-field.ts new file mode 100644 index 000000000..0e2b6b9d6 --- /dev/null +++ b/test/helpers/fill-field.ts @@ -0,0 +1,10 @@ +import SeleniumWebdriver = require("selenium-webdriver"); + +export default function(driver: any, fieldName: string, text: string) { + return driver.wait( + SeleniumWebdriver.until.elementLocated( + SeleniumWebdriver.By.name(fieldName)), 5000) + .then(function (el) { + return el.sendKeys(text); + }); +}; \ No newline at end of file diff --git a/test/helpers/get-identity-link.ts b/test/helpers/get-identity-link.ts new file mode 100644 index 000000000..06381b132 --- /dev/null +++ b/test/helpers/get-identity-link.ts @@ -0,0 +1,34 @@ +import Bluebird = require("bluebird"); +import Fs = require("fs"); +import Request = require("request-promise"); + +export function GetLinkFromFile(): Bluebird { + return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt") + .then(function (data: any) { + const regexp = new RegExp(/Link: (.+)/); + const match = regexp.exec(data); + const link = match[1]; + return Bluebird.resolve(link); + }); +}; + +export function GetLinkFromEmail(): Bluebird { + return Request({ + method: "GET", + uri: "http://localhost:8085/messages", + json: true + }) + .then(function (data: any) { + const messageId = data[data.length - 1].id; + return Request({ + method: "GET", + uri: `http://localhost:8085/messages/${messageId}.html` + }); + }) + .then(function (data: any) { + const regexp = new RegExp(/Continue<\/a>/); + const match = regexp.exec(data); + const link = match[1]; + return Bluebird.resolve(link); + }); +}; \ No newline at end of file diff --git a/test/helpers/register-totp.ts b/test/helpers/register-totp.ts index 9828c8cec..2d84fdf67 100644 --- a/test/helpers/register-totp.ts +++ b/test/helpers/register-totp.ts @@ -1,38 +1,6 @@ import Bluebird = require("bluebird"); import SeleniumWebdriver = require("selenium-webdriver"); -import Fs = require("fs"); -import Request = require("request-promise"); - -function retrieveValidationLinkFromNotificationFile(): Bluebird { - return Bluebird.promisify(Fs.readFile)("/tmp/authelia/notification.txt") - .then(function (data: any) { - const regexp = new RegExp(/Link: (.+)/); - const match = regexp.exec(data); - const link = match[1]; - return Bluebird.resolve(link); - }); -}; - -function retrieveValidationLinkFromEmail(): Bluebird { - return Request({ - method: "GET", - uri: "http://localhost:8085/messages", - json: true - }) - .then(function (data: any) { - const messageId = data[data.length - 1].id; - return Request({ - method: "GET", - uri: `http://localhost:8085/messages/${messageId}.html` - }); - }) - .then(function (data: any) { - const regexp = new RegExp(/Continue<\/a>/); - const match = regexp.exec(data); - const link = match[1]; - return Bluebird.resolve(link); - }); -}; +import {GetLinkFromFile, GetLinkFromEmail} from '../helpers/get-identity-link'; export default function(driver: any, email?: boolean): Bluebird { return driver.wait(SeleniumWebdriver.until.elementLocated(SeleniumWebdriver.By.className("register-totp")), 5000) @@ -40,8 +8,8 @@ export default function(driver: any, email?: boolean): Bluebird { return driver.findElement(SeleniumWebdriver.By.className("register-totp")).click(); }) .then(function () { - if(email) return retrieveValidationLinkFromEmail(); - else return retrieveValidationLinkFromNotificationFile(); + if(email) return GetLinkFromEmail(); + else return GetLinkFromFile(); }) .then(function (link: string) { return driver.get(link); diff --git a/test/minimal-config/00-suite.ts b/test/minimal-config/00-suite.ts index 71f8db899..5fd30c77d 100644 --- a/test/minimal-config/00-suite.ts +++ b/test/minimal-config/00-suite.ts @@ -1,21 +1,32 @@ require("chromedriver"); +import ChildProcess = require('child_process'); +import Bluebird = require("bluebird"); + import Environment = require('../environment'); +const execAsync = Bluebird.promisify(ChildProcess.exec); + const includes = [ "docker-compose.minimal.yml", "example/compose/docker-compose.base.yml", "example/compose/nginx/minimal/docker-compose.yml", - "example/compose/ldap/docker-compose.yml" ] before(function() { this.timeout(20000); this.environment = new Environment.Environment(includes); - return this.environment.setup(2000); + + return execAsync("cp users_database.yml users_database.test.yml") + .then(() => this.environment.setup(2000)); }); after(function() { this.timeout(30000); - return this.environment.cleanup(); + return execAsync("rm users_database.test.yml") + .then(() => { + if(process.env.KEEP_ENV != "true") { + return this.environment.cleanup(); + } + }); }); \ No newline at end of file diff --git a/test/minimal-config/reset_password.ts b/test/minimal-config/reset_password.ts new file mode 100644 index 000000000..5ae3d674e --- /dev/null +++ b/test/minimal-config/reset_password.ts @@ -0,0 +1,43 @@ +require("chromedriver"); +import Bluebird = require("bluebird"); +import ChildProcess = require("child_process"); +import SeleniumWebdriver = require("selenium-webdriver"); + +import WithDriver from '../helpers/with-driver'; +import VisitPage from '../helpers/visit-page'; +import ClickOnLink from '../helpers/click-on-link'; +import ClickOnButton from '../helpers/click-on-button'; +import WaitRedirect from '../helpers/wait-redirected'; +import FillField from "../helpers/fill-field"; +import {GetLinkFromFile} from "../helpers/get-identity-link"; +import FillLoginPageAndClick from "../helpers/fill-login-page-and-click"; + +const execAsync = Bluebird.promisify(ChildProcess.exec); + +describe('Reset password', function() { + this.timeout(10000); + WithDriver(); + + after(() => { + return execAsync("cp users_database.yml users_database.test.yml"); + }) + + describe('click on reset password', function() { + it("should reset password for john", function() { + return VisitPage(this.driver, "https://login.example.com:8080/") + .then(() => ClickOnLink(this.driver, "Forgot password\?")) + .then(() => WaitRedirect(this.driver, "https://login.example.com:8080/password-reset/request")) + .then(() => FillField(this.driver, "username", "john")) + .then(() => ClickOnButton(this.driver, "Reset Password")) + .then(() => this.driver.sleep(1000)) // Simulate the time to read it from mailbox. + .then(() => GetLinkFromFile()) + .then((link) => VisitPage(this.driver, link)) + .then(() => FillField(this.driver, "password1", "newpass")) + .then(() => FillField(this.driver, "password2", "newpass")) + .then(() => ClickOnButton(this.driver, "Reset Password")) + .then(() => WaitRedirect(this.driver, "https://login.example.com:8080/")) + .then(() => FillLoginPageAndClick(this.driver, "john", "newpass")) + .then(() => WaitRedirect(this.driver, "https://login.example.com:8080/secondfactor")) + }); + }); +}); diff --git a/users_database.yml b/users_database.yml new file mode 100644 index 000000000..7832e85b3 --- /dev/null +++ b/users_database.yml @@ -0,0 +1,29 @@ +############################################################### +# Users Database # +############################################################### + +# This file can be used if you do not have an LDAP set up. + +# List of users +users: + john: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: john.doe@authelia.com + groups: + - admins + - dev + + harry: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + emails: harry.potter@authelia.com + groups: [] + + bob: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: bob.dylan@authelia.com + groups: + - dev + + james: + password: "{CRYPT}$6$rounds=500000$jgiCMRyGXzoqpxS3$w2pJeZnnH8bwW3zzvoMWtTRfQYsHbWbD/hquuQ5vUeIyl9gdwBIt6RWk2S6afBA0DPakbeWgD/4SZPiS0hYtU/" + email: james.dean@authelia.com \ No newline at end of file