From 90a8fde200a0c7ae5110d6b435f94f0384ccee71 Mon Sep 17 00:00:00 2001 From: Erik Kristensen Date: Mon, 22 Sep 2014 09:23:10 -0600 Subject: [PATCH] I do not like big commits, but sorta necessary on this, this makes docker registries 0.8.1 and below all work --- app.js | 42 ++++----------- config/default.js | 5 +- endpoints/index_images.js | 13 +++++ endpoints/index_users.js | 13 +---- index/helpers.js | 4 +- index/images.js | 25 +++++++++ index/middleware.js | 35 +++++++++---- index/repos.js | 33 ++++++++++-- index/users.js | 107 ++++++++++++++++++++++++++++---------- internal/middleware.js | 2 +- internal/users.js | 4 ++ lib/firsttime.js | 54 +++++++++++++++++++ lib/upgrades.js | 35 +++++++++++++ package.json | 2 +- 14 files changed, 282 insertions(+), 92 deletions(-) create mode 100644 lib/firsttime.js create mode 100644 lib/upgrades.js diff --git a/app.js b/app.js index 4c59a93..6ef655c 100644 --- a/app.js +++ b/app.js @@ -12,6 +12,10 @@ var logger = bunyan.createLogger({ // Setup Redis Connection var redis = require('redis').createClient(config.redis.port, config.redis.host); +redis.on('error', function(err) { + console.log(err); +}) + // Setup Restify Endpoints var endpoints = new restify_endpoints.EndpointManager({ endpointpath: __dirname + '/endpoints', @@ -24,6 +28,10 @@ var server = restify.createServer({ version: '1.0.0' }); +//server.on('uncaughtException', function (req, res, route, err) { +// console.log('uncaughtException', err.stack); +//}); + // Basic Restify Middleware server.use(restify.acceptParser(server.acceptable)); server.use(restify.queryParser()); @@ -51,35 +59,5 @@ server.listen(config.app.port, function () { console.log('%s listening at %s', server.name, server.url); }); - -// Check and see if the system needs to be initialized, -// if so create a unqiue token to authenticate against the server. -redis.smembers('users', function(err, members) { - if (members.length == 0) { - // We'll assume that the system hasn't been initialzed and generate - // random token used for initial auth from the command line tool. - - require('crypto').randomBytes(24, function(ex, buf) { - var token = buf.toString('hex'); - - redis.set('_initial_auth_token', token, function(err, result) { - redis.expire('_initial_auth_token', 1800, function(err, result) { - console.log('----------------------------------------------------------------------') - console.log() - console.log(' First Time Initialization Detected'); - console.log() - console.log(' You will need the following token to authenticate with the'); - console.log(' command line tool to add your first admin account, this token'); - console.log(' will expire after 30 minutes, if you do not create your account'); - console.log(' within that time, simply restart the server and a new token will'); - console.log(' be generated'); - console.log() - console.log(' Token: ' + token); - console.log() - console.log('----------------------------------------------------------------------') - }); - }); - }); - } -}) - +require('./lib/firsttime.js')(config, redis); +require('./lib/upgrades.js')(config, redis); diff --git a/config/default.js b/config/default.js index ae8f6c0..27c3ef3 100644 --- a/config/default.js +++ b/config/default.js @@ -10,6 +10,7 @@ module.exports = { loglevel: 'debug', registries: [ // format: hostname [, hostname, hostname, hostname] - '192.168.1.114:5000' - ] + 'localhost:5000' + ], + version: '1.2.0' } \ No newline at end of file diff --git a/endpoints/index_images.js b/endpoints/index_images.js index ff1d9c4..d6fcf3e 100644 --- a/endpoints/index_images.js +++ b/endpoints/index_images.js @@ -35,6 +35,19 @@ module.exports = function(config, redis, logger) { middleware: [ index_middleware.requireAuth ] }, + { + name: 'getRepoImagesImageAccess', + description: '', + method: 'GET', + path: [ + '/v1/repositories/:repo/layer/:image/access', + '/v1/repositories/:namespace/:repo/layer/:image/access' + ], + version: '1.0.0', + fn: index_images.repoImagesLayerAccess, + middleware: [ index_middleware.requireAuth ] + } + ] }; diff --git a/endpoints/index_users.js b/endpoints/index_users.js index 7c6737a..e8159e1 100644 --- a/endpoints/index_users.js +++ b/endpoints/index_users.js @@ -15,13 +15,7 @@ module.exports = function(config, redis, logger) { auth: false, path: '/v1/users', version: '1.0.0', - fn: function (req, res, next) { - res.send(200); - return next(); - }, - //middleware: [ - // index_middleware.requireAuth - //] + fn: index_users.validateUser }, { @@ -29,10 +23,7 @@ module.exports = function(config, redis, logger) { description: 'Endpoint from Index Spec, to login or create user', method: 'POST', path: '/v1/users', - fn: index_users.createUser, - middleware: [ - index_middleware.requireAuth - ] + fn: index_users.createUser }, { diff --git a/index/helpers.js b/index/helpers.js index eb9324b..e8d4e6b 100644 --- a/index/helpers.js +++ b/index/helpers.js @@ -11,13 +11,13 @@ module.exports = function(redis) { var sha1 = shasum.digest('hex'); - redis.set("token:" + sha1, JSON.stringify({repo: repo, access: access}), function(err, status) { + redis.set("tokens:" + sha1, JSON.stringify({repo: repo, access: access}), function(err, status) { if (err) cb(err); // TODO: better way to do this? // Set a 10 minute expiration, 10 minutes should be enough time to download images // in the case of a slow internet connection, this could be a problem, but we do not // want unused tokens remaining in the system either - redis.expire("token:" + sha1, 600, function(err, status) { + redis.expire("tokens:" + sha1, 600, function(err, status) { if (err) cb(err); cb(null, sha1); diff --git a/index/images.js b/index/images.js index 24deef4..d5dcdb6 100644 --- a/index/images.js +++ b/index/images.js @@ -1,3 +1,5 @@ +var util = require('util'); + module.exports = function(redis, logger) { return { @@ -32,6 +34,8 @@ module.exports = function(redis, logger) { res.send(500, err); return next(); } + + console.log(req.token_auth); if (status == 0) { redis.sadd('images', name, function(err) { @@ -50,6 +54,27 @@ module.exports = function(redis, logger) { return next(); } }) + }, + + repoImagesLayerAccess: function (req, res, next) { + var key = util.format("tokens:%s:images:%s", req.token_auth.token, req.params.image); + redis.get(key, function(err, result) { + if (err) { + res.send(500, {error: err, access: false}) + return next(); + } + + redis.del(key); + + if (result == "1") { + res.send(200, {access: true}) + return next(); + } + else { + res.send(200, {access: false}) + return next(); + } + }); } } // end return diff --git a/index/middleware.js b/index/middleware.js index d40435b..8a3568c 100644 --- a/index/middleware.js +++ b/index/middleware.js @@ -28,7 +28,7 @@ module.exports = function(config, redis, logger) { shasum.update(pass); var sha1pwd = shasum.digest('hex'); - redis.get("user:" + user, function(err, value) { + redis.get("users:" + user, function(err, value) { if (err) { logger.error({err: err, user: user}); res.send(500, err); @@ -36,13 +36,21 @@ module.exports = function(config, redis, logger) { } if (value == null) { - logger.debug({req: req, permission: req.permission, statusCode: 403, message: 'access denied: user not found'}); + logger.debug({permission: req.permission, statusCode: 403, message: 'access denied: user not found'}); res.send(403, 'access denied (1)') return next(); } value = JSON.parse(value); + // If the account is disabled, do not let it do anything at all + if (value.disabled == true || value.disabled == "true") { + logger.debug({message: "account is disabled", user: value.username}); + res.send(401, {message: "access denied (2)"}) + return next(); + } + + // Check that passwords match if (value.password == sha1pwd) { // TODO: Better handling for non repo images urls if (req.url == '/v1/users/') { @@ -99,6 +107,9 @@ module.exports = function(config, redis, logger) { index_helpers.generateToken(repo, access, function(err, token) { var repo = req.params.namespace + '/' + req.params.repo; + + req.token_auth = {token: token, repo: repo, access: access}; + var token = 'signature=' + token + ', repository="' + repo + '", access=' + access; logger.debug({namespace: req.params.namespace, repo: req.params.repo, token: token, access: access}); @@ -111,7 +122,7 @@ module.exports = function(config, redis, logger) { }) } else { - logger.debug({req: req, statusCode: 401, message: 'access denied: valid authorization information is required'}); + logger.debug({statusCode: 401, message: 'access denied: valid authorization information is required'}); res.send(401, 'Authorization required'); return next(); } @@ -125,7 +136,9 @@ module.exports = function(config, redis, logger) { var repo = matches[1].split('=')[1].replace(/\"/g, ''); var access = matches[2].split('=')[1]; - redis.get("token:" + sig, function(err, value) { + req.token_auth = { token: sig, repo: repo, access: access }; + + redis.get("tokens:" + sig, function(err, value) { if (err) { logger.error({err: err, token: sig}); res.send(500, err); @@ -134,12 +147,12 @@ module.exports = function(config, redis, logger) { value = JSON.parse(value); - redis.del("token:" + sig, function (err) { - if (err) { - logger.error({err: err, token: sig}); - res.send(500, err); - return next(); - } + //redis.del("token:" + sig, function (err) { + // if (err) { + // logger.error({err: err, token: sig}); + // res.send(500, err); + // return next(); + // } if (value.repo == repo && value.access == access) { return next(); @@ -148,7 +161,7 @@ module.exports = function(config, redis, logger) { res.send(401, 'Authorization required'); return next(); } - }); + //}); }); } diff --git a/index/repos.js b/index/repos.js index 5d7acd1..72cd0ad 100644 --- a/index/repos.js +++ b/index/repos.js @@ -1,3 +1,6 @@ +var async = require('async'); +var util = require('util'); + module.exports = function(redis, logger) { return { @@ -5,7 +8,7 @@ module.exports = function(redis, logger) { repoGet: function (req, res, next) { if (!req.params.namespace) req.params.namespace = 'library'; - + if (req.permission != 'admin' && req.permission != 'write') { res.send(403, 'access denied'); return next(); @@ -66,15 +69,37 @@ module.exports = function(redis, logger) { final_images.push(images[key]); } - redis.set('images:' + req.params.namespace + '_' + req.params.repo, JSON.stringify(final_images), function(err, status) { + var image_key = util.format("images:%s_%s", req.params.namespace, req.params.repo); + redis.set(image_key, JSON.stringify(final_images), function(err, status) { if (err) { logger.error({err: err, type: 'redis', namespace: req.params.namespace, repo: req.params.repo}); res.send(500, err); return next(); } - res.send(200, "") - return next(); + async.each(final_images, function(image, cb) { + var token_key = util.format("tokens:%s:images:%s", req.token_auth.token, image.id); + redis.set(token_key, 1, function(err, resp) { + if (err) { + cb(err); + } + redis.expire(token_key, 60, function(err, resp2) { + if (err) { + cb(err); + } + + cb(); + }); + }); + }, function(err) { + if (err) { + res.send(500, "something went wrong"); + return next(); + } + + res.send(200); + return next(); + }); }) }); }, diff --git a/index/users.js b/index/users.js index 8e5e35b..1e94b44 100644 --- a/index/users.js +++ b/index/users.js @@ -4,7 +4,9 @@ var config = require('config'); module.exports = function(redis, logger) { return { createUser: function (req, res, next) { - redis.get("user:" + req.body.username, function(err, value) { + // Validate against a-z0-9_ regexx + + redis.get("users:" + req.body.username, function(err, value) { if (err) { res.send(500, err); return next(); @@ -12,47 +14,54 @@ module.exports = function(redis, logger) { var user = JSON.parse(value) || {}; - // Check to make sure a user was found. - /* - if (user.length == 0) { - res.send(403, {message: "bad username and/or password (1)"}); - return next(); - } - */ - var shasum = crypto.createHash("sha1"); shasum.update(req.body.password); var sha1 = shasum.digest("hex"); - var userObj = {}; + if (user.length == 0) { + // User Does Not Exist, Create! + var userObj = {}; - userObj.username = req.body.username; - userObj.password = sha1; - userObj.email = req.body.email; + userObj.username = user.username || req.body.username; + userObj.password = user.password || sha1; + userObj.email = user.email || req.body.email; + userObj.permissions = user.permissions || {}; - // Check to make sure the password is valid. - if (userObj.password != sha1) { - res.send(403, {message: "bad username and/or password (2)"}); - return next(); - } + if (config.private == true) + userObj.disabled = true; - if (config.private == true) - userObj.disabled = true; + // Check to make sure the password is valid. + if (userObj.password != sha1) { + res.send(400, {message: "bad username and/or password (2)"}); + return next(); + } - redis.set("user:" + userObj.username, JSON.stringify(userObj), function(err, status) { - if (err) { - res.send(500, err); + redis.set("users:" + userObj.username, JSON.stringify(userObj), function(err, status) { + if (err) { + return res.send(500, err); + } + + return res.send(201); + }); + } + else { + var userObj = user; + + if (userObj.password != sha1) { + res.send(400, {message: "bad username and/or password (2)"}); + return next(); + } + else { + res.send(201); return next(); } - res.send(201); - return next(); - }); + } }); }, updateUser: function (req, res, next) { - redis.get("user:" + req.params.username, function(err, value) { + redis.get("users:" + req.params.username, function(err, value) { if (err) { res.send(500, err); return next(); @@ -67,7 +76,7 @@ module.exports = function(redis, logger) { user.password = sha1; user.email = req.body.email; - redis.set("_user_" + req.params.username, JSON.stringify(user), function(err, status) { + redis.set("users:" + req.params.username, JSON.stringify(user), function(err, status) { if (err) { res.send(500, err); return next(); @@ -77,6 +86,48 @@ module.exports = function(redis, logger) { return next(); }); }); + }, + + validateUser: function(req, res, next) { + if (!req.headers.authorization) { + return res.send(401); + } + + var auth = req.headers.authorization.split(' '); + + if (auth[0] == 'Basic') { + var buff = new Buffer(auth[1], 'base64'); + var plain = buff.toString(); + var creds = plain.split(':'); + var username = creds[0]; + var password = creds[1]; + + redis.get("users:" + user, function(err, value) { + if (err) { + res.send(500, err); + return next(); + } + + var user = JSON.parse(value) || {}; + + var shasum = crypto.createHash("sha1"); + shasum.update(req.body.password); + var sha1 = shasum.digest("hex"); + + if (user.disabled == true) { + return res.send(403, {message: "account is not active"}); + } + + // Check to make sure the password is valid. + if (user.password != sha1) { + return res.send(401, {message: "bad username and/or password (2)"}); + } + + return res.send(200); + }); + } + + return res.send(401); } } diff --git a/internal/middleware.js b/internal/middleware.js index d14bbff..b037fe7 100644 --- a/internal/middleware.js +++ b/internal/middleware.js @@ -24,7 +24,7 @@ module.exports = function(config, redis, logger) { shasum.update(pass); var sha1pwd = shasum.digest('hex'); - redis.get("user:" + user, function(err, value) { + redis.get("users:" + user, function(err, value) { if (err) { logger.error({err: err, user: user}); res.send(500, err); diff --git a/internal/users.js b/internal/users.js index 40f4544..9d4c47f 100644 --- a/internal/users.js +++ b/internal/users.js @@ -55,6 +55,10 @@ module.exports = function(redis, logger) { endpoints.addUserPermission = function(req, res, next) { redis.get('user:' + req.params.username, function(err, get_user) { var user = JSON.parse(get_user); + + if (!user.permissions) { + user.permissions = {}; + } user.permissions[req.body.repo] = req.body.access; diff --git a/lib/firsttime.js b/lib/firsttime.js new file mode 100644 index 0000000..9e925a1 --- /dev/null +++ b/lib/firsttime.js @@ -0,0 +1,54 @@ +module.exports = function(config, redis) { + + redis.smembers('users', function(err, members) { + if (members.length == 0) { + // We'll assume that the system hasn't been initialzed and generate + // random token used for initial auth from the command line tool. + + require('crypto').randomBytes(12, function(ex, buf) { + var token = buf.toString('hex'); + + + var shasum = require('crypto').createHash("sha1"); + shasum.update(token); + var sha1 = shasum.digest("hex"); + + var userObj = { + username: 'admin', + password: sha1, + email: 'admin@localhost', + disabled: false, + admin: true, + permissions: { + test: "admin", + admin: "admin", + dockerfile: "admin" + } + }; + + redis.set('users:admin', JSON.stringify(userObj), function(err, result) { + redis.sadd('users', 'admin', function(err, result1) { + console.log('----------------------------------------------------------------------') + console.log() + console.log(' First Time Initialization Detected'); + console.log() + console.log(' Default username and password created') + console.log() + console.log(' User: ' + userObj.username); + console.log(' Pass: ' + token); + console.log() + console.log(' You can push or pull by default to the namespace test/ and admin/') + console.log(' You can login against the repo with these credentials and/or use') + console.log(' the docker-index command line utility to change your password') + console.log(' and further adminsiter the index (users, permissions, etc)') + console.log() + console.log('----------------------------------------------------------------------') + + }); + }); + + }); + } + }); + +}; \ No newline at end of file diff --git a/lib/upgrades.js b/lib/upgrades.js new file mode 100644 index 0000000..c8851de --- /dev/null +++ b/lib/upgrades.js @@ -0,0 +1,35 @@ +var async = require('async'); + +module.exports = function(config, redis) { + + redis.get('version', function(err, version) { + if (version == null) { + async.series([ + function(cb) { + redis.set('version', config.version); + cb(); + }, + function(cb) { + redis.keys("user:*", function(err, users) { + async.each(users, function(user, ecb) { + redis.get(user, function(err, value) { + var info = user.split(':'); + + redis.set("users:" + info[1], value, function(err, result) { + redis.del(user, function(err) { + ecb(); + }); + }); + }); + }, function(err) { + cb(); + }) + }); + } + ], function (err, results) { + console.log('upgrade complete') + }); + } + }); + +}; diff --git a/package.json b/package.json index 905a960..a4d40af 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "restify": "git+https://github.com/ekristen/node-restify.git", "restify-endpoints": "*", "bunyan": "~0.22.1", - "async": "~0.6.1", + "async": "~0.6.2", "config": "~0.4.35", "redis": "~0.10.1", "underscore": "~1.6.0"