First step to typescript transformation

This commit is contained in:
Clement Michaud 2017-05-13 18:12:26 +02:00
parent 6f8b95b0ce
commit 4356cfe7c1
19 changed files with 457 additions and 247 deletions

10
.gitignore vendored
View File

@ -1,8 +1,13 @@
# NodeJs modules
node_modules/ node_modules/
# Coverage reports
coverage/ coverage/
src/.baseDir.ts
.vscode/
*.swp *.swp
*.sh *.sh
@ -11,6 +16,11 @@ config.yml
npm-debug.log npm-debug.log
# Directory used by example
notifications/ notifications/
# VSCode user configuration
.vscode/ .vscode/
# Generated by TypeScript compiler
dist/

49
Gruntfile.js Normal file
View File

@ -0,0 +1,49 @@
module.exports = function(grunt) {
grunt.initConfig({
run: {
options: {},
"build-ts": {
cmd: "npm",
args: ['run', 'build-ts']
},
"tslint": {
cmd: "npm",
args: ['run', 'tslint']
},
"test": {
cmd: "npm",
args: ['run', 'test']
}
},
copy: {
resources: {
expand: true,
cwd: 'src/resources/',
src: '**',
dest: 'dist/src/resources/'
},
views: {
expand: true,
cwd: 'src/views/',
src: '**',
dest: 'dist/src/views/'
},
public_html: {
expand: true,
cwd: 'src/public_html/',
src: '**',
dest: 'dist/src/public_html/'
}
}
});
grunt.loadNpmTasks('grunt-run');
grunt.loadNpmTasks('grunt-contrib-copy');
grunt.registerTask('default', ['build']);
grunt.registerTask('res', ['copy:resources', 'copy:views', 'copy:public_html']);
grunt.registerTask('build', ['run:tslint', 'run:build-ts', 'res']);
grunt.registerTask('test', ['run:test']);
};

View File

@ -7,11 +7,15 @@
"authelia": "src/index.js" "authelia": "src/index.js"
}, },
"scripts": { "scripts": {
"test": "./node_modules/.bin/mocha --recursive test/unitary", "test": "./node_modules/.bin/mocha -r ts-node/register --recursive test/unitary",
"unit-test": "./node_modules/.bin/mocha --recursive test/unitary", "unit-test": "./node_modules/.bin/mocha --recursive test/unitary",
"int-test": "./node_modules/.bin/mocha --recursive test/integration", "int-test": "./node_modules/.bin/mocha --recursive test/integration",
"all-test": "./node_modules/.bin/mocha --recursive test", "all-test": "./node_modules/.bin/mocha --recursive test",
"coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test" "coverage": "./node_modules/.bin/istanbul cover _mocha -- -R spec --recursive test",
"build-ts": "tsc",
"watch-ts": "tsc -w",
"tslint": "tslint -c tslint.json -p tsconfig.json",
"serve": "node dist/src/index.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -43,12 +47,29 @@
"yamljs": "^0.2.8" "yamljs": "^0.2.8"
}, },
"devDependencies": { "devDependencies": {
"@types/assert": "0.0.31",
"@types/bluebird": "^3.5.3",
"@types/express": "^4.0.35",
"@types/express-session": "0.0.32",
"@types/ldapjs": "^1.0.0",
"@types/mocha": "^2.2.41",
"@types/nedb": "^1.8.3",
"@types/nodemailer": "^1.3.32",
"@types/object-path": "^0.9.28",
"@types/sinon": "^2.2.1",
"@types/winston": "^2.3.2",
"@types/yamljs": "^0.2.30",
"grunt": "^1.0.1",
"grunt-contrib-copy": "^1.0.0",
"grunt-run": "^0.6.0",
"mocha": "^3.2.0", "mocha": "^3.2.0",
"mockdate": "^2.0.1", "mockdate": "^2.0.1",
"request": "^2.79.0", "request": "^2.79.0",
"should": "^11.1.1", "should": "^11.1.1",
"sinon": "^1.17.6", "sinon": "^1.17.6",
"sinon-promise": "^0.1.3", "sinon-promise": "^0.1.3",
"tmp": "0.0.31" "tmp": "0.0.31",
"ts-node": "^3.0.4",
"tslint": "^5.2.0"
} }
} }

View File

@ -1,36 +0,0 @@
#! /usr/bin/env node
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var server = require('./lib/server');
var ldapjs = require('ldapjs');
var u2f = require('authdog');
var nodemailer = require('nodemailer');
var nedb = require('nedb');
var YAML = require('yamljs');
var session = require('express-session');
var winston = require('winston');
var speakeasy = require('speakeasy');
var config_path = process.argv[2];
if(!config_path) {
console.log('No config file has been provided.');
console.log('Usage: authelia <config>');
process.exit(0);
}
console.log('Parse configuration file: %s', config_path);
var yaml_config = YAML.load(config_path);
var deps = {};
deps.u2f = u2f;
deps.nedb = nedb;
deps.nodemailer = nodemailer;
deps.ldapjs = ldapjs;
deps.session = session;
deps.winston = winston;
deps.speakeasy = speakeasy;
server.run(yaml_config, deps);

29
src/index.ts Executable file
View File

@ -0,0 +1,29 @@
#! /usr/bin/env node
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
import * as server from "./lib/server";
const YAML = require("yamljs");
const config_path = process.argv[2];
if (!config_path) {
console.log("No config file has been provided.");
console.log("Usage: authelia <config>");
process.exit(0);
}
console.log("Parse configuration file: %s", config_path);
const yaml_config = YAML.load(config_path);
const deps = {
u2f: require("authdog"),
nodemailer: require("nodemailer"),
ldapjs: require("ldapjs"),
session: require("express-session"),
winston: require("winston"),
speakeasy: require("speakeasy"),
nedb: require("nedb")
};
server.run(yaml_config, deps);

View File

@ -1,17 +0,0 @@
var objectPath = require('object-path');
module.exports = function(yaml_config) {
return {
port: objectPath.get(yaml_config, 'port', 8080),
ldap: objectPath.get(yaml_config, 'ldap', 'ldap://127.0.0.1:389'),
session_domain: objectPath.get(yaml_config, 'session.domain'),
session_secret: objectPath.get(yaml_config, 'session.secret'),
session_max_age: objectPath.get(yaml_config, 'session.expiration', 3600000), // in ms
store_directory: objectPath.get(yaml_config, 'store_directory'),
logs_level: objectPath.get(yaml_config, 'logs_level'),
notifier: objectPath.get(yaml_config, 'notifier'),
access_control: objectPath.get(yaml_config, 'access_control')
}
};

38
src/lib/config_adapter.ts Normal file
View File

@ -0,0 +1,38 @@
import * as ObjectPath from "object-path";
import { authelia } from "../types/authelia";
function get_optional<T>(config: object, path: string, default_value: T): T {
let entry = default_value;
if (ObjectPath.has(config, path)) {
entry = ObjectPath.get<object, T>(config, path);
}
return entry;
}
function ensure_key_existence(config: object, path: string): void {
if (!ObjectPath.has(config, path)) {
throw new Error(`Configuration error: key '${path}' is missing in configuration file`);
}
}
export = function(yaml_config: object): authelia.Configuration {
ensure_key_existence(yaml_config, "ldap");
ensure_key_existence(yaml_config, "session.secret");
const port = ObjectPath.get(yaml_config, "port", 8080);
return {
port: port,
ldap: ObjectPath.get(yaml_config, "ldap"),
session_domain: ObjectPath.get<object, string>(yaml_config, "session.domain"),
session_secret: ObjectPath.get<object, string>(yaml_config, "session.secret"),
session_max_age: get_optional<number>(yaml_config, "session.expiration", 3600000), // in ms
store_directory: get_optional<string>(yaml_config, "store_directory", undefined),
logs_level: get_optional<string>(yaml_config, "logs_level", "info"),
notifier: ObjectPath.get<object, authelia.NotifiersConfiguration>(yaml_config, "notifier"),
access_control: ObjectPath.get<object, authelia.ACLConfiguration>(yaml_config, "access_control")
};
};

View File

@ -106,7 +106,7 @@ function identity_check_post(endpoint, icheck_interface) {
return identity_check.issue_token(userid, undefined, logger); return identity_check.issue_token(userid, undefined, logger);
}, function(err) { }, function(err) {
throw new exceptions.AccessDeniedError(); throw new exceptions.AccessDeniedError(err);
}) })
.then(function(token) { .then(function(token) {
var redirect_url = objectPath.get(req, 'body.redirect'); var redirect_url = objectPath.get(req, 'body.redirect');
@ -124,12 +124,12 @@ function identity_check_post(endpoint, icheck_interface) {
res.send(); res.send();
}) })
.catch(exceptions.IdentityError, function(err) { .catch(exceptions.IdentityError, function(err) {
logger.error('POST identity_check: IdentityError %s', err); logger.error('POST identity_check: %s', err);
res.status(400); res.status(400);
res.send(); res.send();
}) })
.catch(exceptions.AccessDeniedError, function(err) { .catch(exceptions.AccessDeniedError, function(err) {
logger.error('POST identity_check: AccessDeniedError %s', err); logger.error('POST identity_check: %s', err);
res.status(403); res.status(403);
res.send(); res.send();
}) })

View File

@ -1,5 +1,5 @@
module.exports = totp; module.exports = totp_fn;
var totp = require('../totp'); var totp = require('../totp');
var objectPath = require('object-path'); var objectPath = require('object-path');
@ -7,7 +7,7 @@ var exceptions = require('../../../src/lib/exceptions');
var UNAUTHORIZED_MESSAGE = 'Unauthorized access'; var UNAUTHORIZED_MESSAGE = 'Unauthorized access';
function totp(req, res) { function totp_fn(req, res) {
var logger = req.app.get('logger'); var logger = req.app.get('logger');
var userid = objectPath.get(req, 'session.auth_session.userid'); var userid = objectPath.get(req, 'session.auth_session.userid');
logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid); logger.info('POST 2ndfactor totp: Initiate TOTP validation for user %s', userid);

View File

@ -1,73 +0,0 @@
module.exports = {
run: run
}
var express = require('express');
var bodyParser = require('body-parser');
var path = require('path');
var UserDataStore = require('./user_data_store');
var Notifier = require('./notifier');
var AuthenticationRegulator = require('./authentication_regulator');
var setup_endpoints = require('./setup_endpoints');
var config_adapter = require('./config_adapter');
var Ldap = require('./ldap');
var AccessControl = require('./access_control');
function run(yaml_config, deps, fn) {
var config = config_adapter(yaml_config);
var view_directory = path.resolve(__dirname, '../views');
var public_html_directory = path.resolve(__dirname, '../public_html');
var datastore_options = {};
datastore_options.directory = config.store_directory;
if(config.store_in_memory)
datastore_options.inMemory = true;
var app = express();
app.use(express.static(public_html_directory));
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
app.set('trust proxy', 1); // trust first proxy
app.use(deps.session({
secret: config.session_secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session_max_age,
domain: config.session_domain
},
}));
app.set('views', view_directory);
app.set('view engine', 'ejs');
// by default the level of logs is info
deps.winston.level = config.logs_level || 'info';
var five_minutes = 5 * 60;
var data_store = new UserDataStore(deps.nedb, datastore_options);
var regulator = new AuthenticationRegulator(data_store, five_minutes);
var notifier = new Notifier(config.notifier, deps);
var ldap = new Ldap(deps, config.ldap);
var access_control = AccessControl(deps.winston, config.access_control);
app.set('logger', deps.winston);
app.set('ldap', ldap);
app.set('totp engine', deps.speakeasy);
app.set('u2f', deps.u2f);
app.set('user data store', data_store);
app.set('notifier', notifier);
app.set('authentication regulator', regulator);
app.set('config', config);
app.set('access control', access_control);
setup_endpoints(app);
return app.listen(config.port, function(err) {
console.log('Listening on %d...', config.port);
if(fn) fn();
});
}

70
src/lib/server.ts Normal file
View File

@ -0,0 +1,70 @@
import { authelia } from "../types/authelia";
import * as Express from "express";
import * as BodyParser from "body-parser";
import * as Path from "path";
const UserDataStore = require("./user_data_store");
const Notifier = require("./notifier");
const AuthenticationRegulator = require("./authentication_regulator");
const setup_endpoints = require("./setup_endpoints");
const config_adapter = require("./config_adapter");
const Ldap = require("./ldap");
const AccessControl = require("./access_control");
export function run(yaml_configuration: authelia.Configuration, deps: authelia.GlobalDependencies, fn?: () => undefined) {
const config = config_adapter(yaml_configuration);
const view_directory = Path.resolve(__dirname, "../views");
const public_html_directory = Path.resolve(__dirname, "../public_html");
const datastore_options = {
directory: config.store_directory,
inMemory: config.store_in_memory
};
const app = Express();
app.use(Express.static(public_html_directory));
app.use(BodyParser.urlencoded({ extended: false }));
app.use(BodyParser.json());
app.set("trust proxy", 1); // trust first proxy
app.use(deps.session({
secret: config.session_secret,
resave: false,
saveUninitialized: true,
cookie: {
secure: false,
maxAge: config.session_max_age,
domain: config.session_domain
},
}));
app.set("views", view_directory);
app.set("view engine", "ejs");
// by default the level of logs is info
deps.winston.level = config.logs_level || "info";
const five_minutes = 5 * 60;
const data_store = new UserDataStore(deps.nedb, datastore_options);
const regulator = new AuthenticationRegulator(data_store, five_minutes);
const notifier = new Notifier(config.notifier, deps);
const ldap = new Ldap(deps, config.ldap);
const access_control = AccessControl(deps.winston, config.access_control);
app.set("logger", deps.winston);
app.set("ldap", ldap);
app.set("totp engine", deps.speakeasy);
app.set("u2f", deps.u2f);
app.set("user data store", data_store);
app.set("notifier", notifier);
app.set("authentication regulator", regulator);
app.set("config", config);
app.set("access control", access_control);
setup_endpoints(app);
return app.listen(config.port, function(err: string) {
console.log("Listening on %d...", config.port);
if (fn) fn();
});
}

View File

@ -1,35 +0,0 @@
module.exports = {
'promisify': promisify,
'resolve': resolve,
'reject': reject
}
var Q = require('q');
function promisify(fn, context) {
return function() {
var defer = Q.defer();
var args = Array.prototype.slice.call(arguments);
args.push(function(err, val) {
if (err !== null && err !== undefined) {
return defer.reject(err);
}
return defer.resolve(val);
});
fn.apply(context || {}, args);
return defer.promise;
};
}
function resolve(data) {
var defer = Q.defer();
defer.resolve(data);
return defer.promise;
}
function reject(err) {
var defer = Q.defer();
defer.reject(err);
return defer.promise;
}

62
src/types/authelia.d.ts vendored Normal file
View File

@ -0,0 +1,62 @@
import * as winston from "winston";
import * as nedb from "nedb";
declare namespace authelia {
interface LdapConfiguration {
url: string;
base_dn: string;
additional_user_dn?: string;
user_name_attribute?: string; // cn by default
additional_group_dn?: string;
group_name_attribute?: string; // cn by default
user: string; // admin username
password: string; // admin password
}
type UserName = string;
type GroupName = string;
type DomainPattern = string;
type ACLDefaultRules = Array<DomainPattern>;
type ACLGroupsRules = Map<GroupName, DomainPattern>;
type ACLUsersRules = Map<UserName, DomainPattern>;
export interface ACLConfiguration {
default: ACLDefaultRules;
groups: ACLGroupsRules;
users: ACLUsersRules;
}
interface SessionCookieConfiguration {
secret: string;
expiration: number;
domain: string
}
type NotifierType = string;
export type NotifiersConfiguration = Map<NotifierType, any>;
export interface Configuration {
port: number;
logs_level: string;
ldap: LdapConfiguration | {};
session_domain?: string;
session_secret: string;
session_max_age: number;
store_directory?: string;
notifier: NotifiersConfiguration;
access_control: ACLConfiguration;
}
export interface GlobalDependencies {
u2f: object;
nodemailer: any;
ldapjs: object;
session: any;
winston: winston.Winston;
speakeasy: object;
nedb: object;
}
}

View File

@ -0,0 +1,88 @@
import * as mocha from "mocha";
import * as Assert from "assert";
const config_adapter = require("../../src/lib/config_adapter");
describe("test config adapter", function() {
function build_yaml_config(): any {
const yaml_config = {
port: 8080,
ldap: {},
session: {
domain: "example.com",
secret: "secret",
max_age: 40000
},
store_directory: "/mydirectory",
logs_level: "debug"
};
return yaml_config;
}
it("should read the port from the yaml file", function() {
const yaml_config = build_yaml_config();
yaml_config.port = 7070;
const config = config_adapter(yaml_config);
Assert.equal(config.port, 7070);
});
it("should default the port to 8080 if not provided", function() {
const yaml_config = build_yaml_config();
delete yaml_config.port;
const config = config_adapter(yaml_config);
Assert.equal(config.port, 8080);
});
it("should get the ldap attributes", function() {
const yaml_config = build_yaml_config();
yaml_config.ldap = {
url: "http://ldap",
user_search_base: "ou=groups,dc=example,dc=com",
user_search_filter: "uid",
user: "admin",
password: "pass"
};
const config = config_adapter(yaml_config);
Assert.equal(config.ldap.url, "http://ldap");
Assert.equal(config.ldap.user_search_base, "ou=groups,dc=example,dc=com");
Assert.equal(config.ldap.user_search_filter, "uid");
Assert.equal(config.ldap.user, "admin");
Assert.equal(config.ldap.password, "pass");
});
it("should get the session attributes", function() {
const yaml_config = build_yaml_config();
yaml_config.session = {
domain: "example.com",
secret: "secret",
expiration: 3600
};
const config = config_adapter(yaml_config);
Assert.equal(config.session_domain, "example.com");
Assert.equal(config.session_secret, "secret");
Assert.equal(config.session_max_age, 3600);
});
it("should get the log level", function() {
const yaml_config = build_yaml_config();
yaml_config.logs_level = "debug";
const config = config_adapter(yaml_config);
Assert.equal(config.logs_level, "debug");
});
it("should get the notifier config", function() {
const yaml_config = build_yaml_config();
yaml_config.notifier = "notifier";
const config = config_adapter(yaml_config);
Assert.equal(config.notifier, "notifier");
});
it("should get the access_control config", function() {
const yaml_config = build_yaml_config();
yaml_config.access_control = "access_control";
const config = config_adapter(yaml_config);
Assert.equal(config.access_control, "access_control");
});
});

View File

@ -1,76 +0,0 @@
var assert = require('assert');
var config_adapter = require('../../src/lib/config_adapter');
describe('test config adapter', function() {
it('should read the port from the yaml file', function() {
yaml_config = {};
yaml_config.port = 7070;
var config = config_adapter(yaml_config);
assert.equal(config.port, 7070);
});
it('should default the port to 8080 if not provided', function() {
yaml_config = {};
var config = config_adapter(yaml_config);
assert.equal(config.port, 8080);
});
it('should get the ldap attributes', function() {
yaml_config = {};
yaml_config.ldap = {};
yaml_config.ldap.url = 'http://ldap';
yaml_config.ldap.user_search_base = 'ou=groups,dc=example,dc=com';
yaml_config.ldap.user_search_filter = 'uid';
yaml_config.ldap.user = 'admin';
yaml_config.ldap.password = 'pass';
var config = config_adapter(yaml_config);
assert.equal(config.ldap.url, 'http://ldap');
assert.equal(config.ldap.user_search_base, 'ou=groups,dc=example,dc=com');
assert.equal(config.ldap.user_search_filter, 'uid');
assert.equal(config.ldap.user, 'admin');
assert.equal(config.ldap.password, 'pass');
});
it('should get the session attributes', function() {
yaml_config = {};
yaml_config.session = {};
yaml_config.session.domain = 'example.com';
yaml_config.session.secret = 'secret';
yaml_config.session.expiration = 3600;
var config = config_adapter(yaml_config);
assert.equal(config.session_domain, 'example.com');
assert.equal(config.session_secret, 'secret');
assert.equal(config.session_max_age, 3600);
});
it('should get the log level', function() {
yaml_config = {};
yaml_config.logs_level = 'debug';
var config = config_adapter(yaml_config);
assert.equal(config.logs_level, 'debug');
});
it('should get the notifier config', function() {
yaml_config = {};
yaml_config.notifier = 'notifier';
var config = config_adapter(yaml_config);
assert.equal(config.notifier, 'notifier');
});
it('should get the access_control config', function() {
yaml_config = {};
yaml_config.access_control = 'access_control';
var config = config_adapter(yaml_config);
assert.equal(config.access_control, 'access_control');
});
});

View File

@ -12,8 +12,6 @@ var session = require('express-session');
var winston = require('winston'); var winston = require('winston');
var PORT = 8050; var PORT = 8050;
var BASE_URL = 'http://localhost:' + PORT;
var requests = require('./requests')(PORT); var requests = require('./requests')(PORT);

View File

@ -41,6 +41,7 @@ describe('test server configuration', function() {
it('should set cookie scope to domain set in the config', function() { it('should set cookie scope to domain set in the config', function() {
config.session = {}; config.session = {};
config.session.domain = 'example.com'; config.session.domain = 'example.com';
config.session.secret = 'secret';
config.ldap = {}; config.ldap = {};
config.ldap.url = 'http://ldap'; config.ldap.url = 'http://ldap';
server.run(config, deps); server.run(config, deps);

21
tsconfig.json Normal file
View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": true,
"moduleResolution": "node",
"sourceMap": true,
"outDir": "dist",
"baseUrl": ".",
"allowJs": true,
"paths": {
"*": [
"node_modules/*",
"src/types/*"
]
}
},
"include": [
"src/**/*"
]
}

60
tslint.json Normal file
View File

@ -0,0 +1,60 @@
{
"rules": {
"class-name": true,
"comment-format": [
true,
"check-space"
],
"indent": [
true,
"spaces"
],
"one-line": [
true,
"check-open-brace",
"check-whitespace"
],
"no-var-keyword": true,
"quotemark": [
true,
"double",
"avoid-escape"
],
"semicolon": [
true,
"always",
"ignore-bound-class-methods"
],
"whitespace": [
true,
"check-branch",
"check-decl",
"check-operator",
"check-module",
"check-separator",
"check-type"
],
"typedef-whitespace": [
true,
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
],
"no-internal-module": true,
"no-trailing-whitespace": true,
"no-null-keyword": true,
"prefer-const": true,
"jsdoc-format": true
}
}