2018-08-26 15:30:43 +07:00
|
|
|
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<any> {
|
|
|
|
return new Bluebird<string>((resolve, reject) => {
|
|
|
|
this.queue.read((err: Error, data: string) => {
|
|
|
|
if (err) {
|
|
|
|
reject(err);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
resolve(data);
|
|
|
|
this.queue.next();
|
|
|
|
});
|
|
|
|
})
|
2019-01-08 17:15:20 +07:00
|
|
|
.then((content) => {
|
|
|
|
const database = Yaml.parse(content);
|
|
|
|
if (!database) {
|
|
|
|
return Bluebird.reject(new Error("Unable to parse YAML file."));
|
|
|
|
}
|
|
|
|
return Bluebird.resolve(database);
|
|
|
|
});
|
2018-08-26 15:30:43 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks the user exists in the database.
|
|
|
|
*/
|
|
|
|
private checkUserExists(
|
|
|
|
database: any,
|
|
|
|
username: string)
|
|
|
|
: Bluebird<void> {
|
|
|
|
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<void> {
|
|
|
|
const storedHash: string = database.users[username].password;
|
2019-01-08 17:15:20 +07:00
|
|
|
const matches = storedHash.match(/rounds=([0-9]+)\$([a-zA-z0-9.]+)\$/);
|
2018-08-26 15:30:43 +07:00
|
|
|
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<string[]> {
|
|
|
|
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<string[]> {
|
|
|
|
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<void> {
|
|
|
|
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<GroupsAndEmails> {
|
|
|
|
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 };
|
|
|
|
});
|
|
|
|
});
|
2019-01-08 17:15:20 +07:00
|
|
|
});
|
2018-08-26 15:30:43 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
getEmails(username: string): Bluebird<string[]> {
|
|
|
|
return this.readDatabase()
|
|
|
|
.then((database) => {
|
|
|
|
return this.checkUserExists(database, username)
|
|
|
|
.then(() => this.retrieveEmails(database, username));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getGroups(username: string): Bluebird<string[]> {
|
|
|
|
return this.readDatabase()
|
|
|
|
.then((database) => {
|
|
|
|
return this.checkUserExists(database, username)
|
|
|
|
.then(() => this.retrieveGroups(database, username));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
updatePassword(username: string, newPassword: string): Bluebird<void> {
|
|
|
|
return this.readDatabase()
|
|
|
|
.then((database) => {
|
|
|
|
return this.checkUserExists(database, username)
|
|
|
|
.then(() => this.replacePassword(database, username, newPassword));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|