diff --git a/browserid/lib/db_mysql.js b/browserid/lib/db_mysql.js index 5533c26bd330a3e021985e8adf213f4fe2d0b87b..9c41b89960541f41993f6d75fa4676129a3ae4c9 100644 --- a/browserid/lib/db_mysql.js +++ b/browserid/lib/db_mysql.js @@ -6,7 +6,7 @@ /* * The Schema: * - * +--- user ------+ +--- email ----+ +----- key ------+ + * +--- user ------+ +--- email ----+ +--- pubkey -----+ * |*int id | <-\ |*int id | <-\ |*int id | * | string passwd | \- |*int user | \-- |*int email | * +---------------+ |*string address | string pubkey | @@ -18,7 +18,7 @@ * |*string secret | * | bool new_acct | * | string existing | - * | string email | + * |*string email | * | string pubkey | * | string passwd | * | timestamp ts | @@ -27,7 +27,8 @@ const mysql = require('mysql'), -secrets = require('./secrets'); +secrets = require('./secrets'), +logger = require('../../libs/logging.js'); var client = undefined; @@ -35,12 +36,21 @@ var client = undefined; var drop_on_close = undefined; const schemas = [ - "CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY, passwd VARCHAR(64) );", - "CREATE TABLE IF NOT EXISTS email ( id INTEGER PRIMARY KEY, user INTEGER, address VARCHAR(255) UNIQUE, INDEX(address) );", - "CREATE TABLE IF NOT EXISTS pubkey ( id INTEGER PRIMARY KEY, email INTEGER, content TEXT, expiry INTEGER );", + "CREATE TABLE IF NOT EXISTS user ( id INTEGER AUTO_INCREMENT PRIMARY KEY, passwd VARCHAR(64) );", + "CREATE TABLE IF NOT EXISTS email ( id INTEGER AUTO_INCREMENT PRIMARY KEY, user INTEGER, address VARCHAR(255) UNIQUE, INDEX(address) );", + "CREATE TABLE IF NOT EXISTS pubkey ( id INTEGER AUTO_INCREMENT PRIMARY KEY, email INTEGER, content TEXT, expiry DATETIME );", "CREATE TABLE IF NOT EXISTS staged ( secret VARCHAR(48) PRIMARY KEY, new_acct BOOL, existing VARCHAR(255), email VARCHAR(255) UNIQUE, INDEX(email), pubkey TEXT, passwd VARCHAR(64), ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP);" ]; +// log an unexpected database error +function logUnexpectedError(detail) { + // first, get line number of callee + var where; + try { dne; } catch (e) { where = e.stack.split('\n')[2]; }; + // now log it! + logger.log('db', { type: "unexpected", message: "unexpected database failure", detail: detail, where: where }); +} + // open & create the mysql database exports.open = function(cfg, cb) { if (client) throw "database is already open!"; @@ -135,14 +145,6 @@ exports.isStaged = function(email, cb) { ); } -exports.emailsBelongToSameAccount = function() { - throw "not implemented"; -} - -exports.addKeyToEmail = function() { - throw "not implemented"; -} - exports.stageUser = function(obj, cb) { var secret = secrets.generate(48); // overwrite previously staged users @@ -155,10 +157,6 @@ exports.stageUser = function(obj, cb) { }); } -exports.stageEmail = function() { - throw "not implemented"; -} - exports.gotVerificationSecret = function(secret, cb) { client.query( "SELECT * FROM staged WHERE secret = ?", [ secret ], @@ -168,26 +166,115 @@ exports.gotVerificationSecret = function(secret, cb) { else { var o = rows[0]; + function addEmailAndPubkey(userID) { + client.query( + "INSERT INTO email(user, address) VALUES(?, ?)", + [ userID, o.email ], + function(err, info) { + if (err) { cb(err); return; } + addKeyToEmailRecord(info.insertId, o.pubkey, cb); + }); + } + // delete the record client.query("DELETE LOW_PRIORITY FROM staged WHERE secret = ?", [ secret ]); - // XXX: perform add acct or email depending on value of new_acct BOOL - console.log(o); + if (o.new_acct) { + // we're creating a new account, add appropriate entries into user, email, and pubkey. + client.query( + "INSERT INTO user(passwd) VALUES(?)", + [ o.passwd ], + function(err, info) { + if (err) { cb(err); return; } + addEmailAndPubkey(info.insertId); + }); + } else { + // we're adding an email address to an existing user account. add appropriate entries into email and + // pubkey + client.query( + "SELECT user FROM email WHERE address = ?", [ o.existing ], + function(err, rows) { + if (err) cb(err); + else if (rows.length === 0) cb("cannot find email address: " + o.existing); + else { + addEmailAndPubkey(rows[0].user); + } + }); + } } } ); } -exports.checkAuth = function() { +exports.emailsBelongToSameAccount = function(lhs, rhs, cb) { + client.query( + 'SELECT COUNT(*) AS n FROM email WHERE address = ? AND user = ( SELECT user FROM email WHERE address = ? );', + [ lhs, rhs ], + function (err, rows) { + if (err) cb(false); + else cb(rows.length === 1 && rows[0].n === 1); + }); +} + +function addKeyToEmailRecord(emailId, pubkey, cb) { + client.query( + // XXX: 2 weeks is wrong, but then so is keypairs. + "INSERT INTO pubkey(email, content, expiry) VALUES(?, ?, DATE_ADD(NOW(), INTERVAL 2 WEEK))", + [ emailId, pubkey ], + function(err, info) { + cb(err); + }); +} + +exports.addKeyToEmail = function(existing_email, email, pubkey, cb) { + // this function will NOT add a new email address to a user record. The only + // way that happens is when a verification secret is provided to us. Limiting + // the code paths that result in us concluding that a user owns an email address + // is a Good Thing. + exports.emailsBelongToSameAccount(existing_email, email, function(ok) { + if (!ok) { cb("authenticated user doesn't have permission to add a public key to " + email); return; } + + // now we know that the user has permission to add a key. + client.query( + "SELECT id FROM email WHERE address = ?", [ email ], + function(err, rows) { + if (err) cb(err); + else if (rows.length === 0) cb("cannot find email address: " + email); + else { + addKeyToEmailRecord(rows[0].id, pubkey, cb); + } + }); + }); +} + +exports.stageEmail = function() { throw "not implemented"; } +exports.checkAuth = function(email, cb) { + client.query( + 'SELECT passwd FROM user WHERE id = ( SELECT user FROM email WHERE address = ? )', + [ email ], + function (err, rows) { + if (err) logUnexpectedError(err); + cb((rows && rows.length == 1) ? rows[0].passwd : undefined); + }); +} + exports.getSyncResponse = function() { throw "not implemented"; } -exports.pubkeysForEmail = function() { - throw "not implemented"; +exports.pubkeysForEmail = function(email, cb) { + client.query( + 'SELECT content FROM pubkey WHERE email = (SELECT id FROM email WHERE address = ?)', + [ email ], + function (err, rows) { + var ar = [ ]; + if (!err) rows.forEach(function(r) { ar.push(r.content); }); + else logUnexpectedError(err); + cb(ar); + }); } exports.removeEmail = function() { diff --git a/libs/logging.js b/libs/logging.js index c34aaa0c15ad10cd60c1f1c93b3f7968fb02dc0e..1d18ef520ea635ec36a57ed302728a543e37a6a7 100644 --- a/libs/logging.js +++ b/libs/logging.js @@ -35,7 +35,7 @@ exports.log = function(category, entry) { // timestamp entry.at = new Date().toUTCString(); - + // if no logger, go to console (FIXME: do we really want to log to console?) LOGGERS[category].info(JSON.stringify(entry)); };