├── demo ├── server │ ├── .gitignore │ ├── package.json │ ├── README.md │ ├── config.js │ ├── bin │ │ └── www │ ├── routes │ │ ├── router-utils.js │ │ └── service-auth.js │ └── app.js └── client │ ├── keys │ ├── test-ES256-app-pubkey.pem │ └── test-ES256-app-privkey.pem │ ├── README.rd │ ├── package.json │ ├── ecdh-test.js │ └── auth-test.js ├── doc ├── spartanX.png ├── spartan-1.png ├── spartan-2.png ├── highlevel-flow.png ├── spartan-server-schema.png ├── README.md ├── spartan-cli.markdown ├── architecture.md ├── attestation-apis.md ├── getting-started.markdown ├── jwt-tokens.md ├── identity-bootstrapping.md ├── security.md ├── spartanX.md └── identity-bootstrapping.svg ├── .gitignore ├── server ├── views │ ├── error.jade │ └── layout.jade ├── keys │ ├── test-ES256-AS-pubkey.pem │ └── test-ES256-AS-privkey.pem ├── syncdb.js ├── models │ ├── user.js │ ├── user-group.js │ ├── app-in-role.js │ ├── user-in-group.js │ ├── app.js │ ├── member-in-app.js │ ├── index.js │ └── role.js ├── bin │ ├── www │ └── www-admin ├── routes │ ├── router-utils.js │ ├── index.js │ ├── attestation-service.js │ ├── user-auth.js │ ├── user.js │ ├── app-group.js │ ├── role-group.js │ └── user-group.js ├── config.js ├── app-admin.js ├── app.js ├── setup-auto.sql └── spartan.sql ├── tests ├── spartan.js └── auth.js ├── package.json ├── .travis.yml ├── LICENSE.txt └── README.md /demo/server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.key 3 | config.js.bkp 4 | *.swp 5 | *.swo 6 | -------------------------------------------------------------------------------- /doc/spartanX.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YahooArchive/spartan/HEAD/doc/spartanX.png -------------------------------------------------------------------------------- /doc/spartan-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YahooArchive/spartan/HEAD/doc/spartan-1.png -------------------------------------------------------------------------------- /doc/spartan-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YahooArchive/spartan/HEAD/doc/spartan-2.png -------------------------------------------------------------------------------- /doc/highlevel-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YahooArchive/spartan/HEAD/doc/highlevel-flow.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.key 3 | config.js.bkp 4 | *.swp 5 | *.swo 6 | *.log 7 | *.crt 8 | *.csr 9 | -------------------------------------------------------------------------------- /doc/spartan-server-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YahooArchive/spartan/HEAD/doc/spartan-server-schema.png -------------------------------------------------------------------------------- /server/views/error.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | h1= message 5 | h2= error.status 6 | pre #{error.stack} 7 | -------------------------------------------------------------------------------- /server/views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | title= title 5 | link(rel='stylesheet', href='/stylesheets/style.css') 6 | body 7 | block content 8 | -------------------------------------------------------------------------------- /server/keys/test-ES256-AS-pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKNp4XYNdQFqzIgq3ESMNmbPe7VZ/ 3 | Z9VuU8LWwXqsLfMs+ECJIZ3j2KSXKPf7umtTBIiXI70A19pGNPuGaqSa4g== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /demo/client/keys/test-ES256-app-pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEa3w9abRVItYQHz5XY+GBLqYnEdaA 3 | uEMBOX1AL6jqmFkQDC1coy1PyFg3Tijn7j9a6ulPqltz3O+B1G44W8DGww== 4 | -----END PUBLIC KEY----- 5 | -------------------------------------------------------------------------------- /doc/README.md: -------------------------------------------------------------------------------- 1 | ### 2 | 3 | * [Getting started](./getting-started.markdown) 4 | * [Spartan command line tool](./spartan-cli.markdown) 5 | * [Architecture](./architecture.md) 6 | * [Security](./security.md) 7 | * [Attestation service APIs](./attestation-apis.md) 8 | * [JWT types](./jwt-tokens.md) 9 | * Deployment Guide 10 | -------------------------------------------------------------------------------- /server/keys/test-ES256-AS-privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEIF6Cx0WQS6jg/E7wkz/58mqZSXh3x4MMcsy76rzS/fSboAoGCCqGSM49 6 | AwEHoUQDQgAEKNp4XYNdQFqzIgq3ESMNmbPe7VZ/Z9VuU8LWwXqsLfMs+ECJIZ3j 7 | 2KSXKPf7umtTBIiXI70A19pGNPuGaqSa4g== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /demo/client/keys/test-ES256-app-privkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PARAMETERS----- 2 | BggqhkjOPQMBBw== 3 | -----END EC PARAMETERS----- 4 | -----BEGIN EC PRIVATE KEY----- 5 | MHcCAQEEILnUqtXKwr4rNS5tAf0XbxKCs38AD8LzNMCgB+Z15z3toAoGCCqGSM49 6 | AwEHoUQDQgAEa3w9abRVItYQHz5XY+GBLqYnEdaAuEMBOX1AL6jqmFkQDC1coy1P 7 | yFg3Tijn7j9a6ulPqltz3O+B1G44W8DGww== 8 | -----END EC PRIVATE KEY----- 9 | -------------------------------------------------------------------------------- /demo/client/README.rd: -------------------------------------------------------------------------------- 1 | # Test client application 2 | 3 | Create app key pairs: 4 | 5 | To generate ecdsa keypair using openssl, run the following commands: 6 | ``` 7 | % openssl ecparam -name secp384r1 -genkey -out test-priv.key 8 | % openssl ec -in test-priv.key -pubout -out test-pub.key 9 | ``` 10 | 11 | Run: 12 | 13 | ``` 14 | % nodejs index.js 15 | ``` 16 | 17 | -------------------------------------------------------------------------------- /server/syncdb.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | 8 | var models = require('./models'); 9 | models.sequelize.sync({ 10 | force: true, 11 | logging: console.log 12 | }).then(function () { 13 | console.log("DB Sync done.."); 14 | process.exit(0); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/spartan.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | var a = require('../lib/client/spartan'); 7 | 8 | a.init('./test-priv.key', './test-pub.key'); 9 | a.getCert('SuperRole', function(certs) { 10 | //var c = JSON.parse(certs); 11 | //console.log('Certs : ' + JSON.stringify(certs, null, 4)) 12 | console.log('Certs : ' + certs) 13 | }); -------------------------------------------------------------------------------- /demo/server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spartan-demo-appserver", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node ./bin/www" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.8.1", 10 | "cookie-parser": "~1.3.3", 11 | "debug": "~2.0.0", 12 | "express": "~4.9.0", 13 | "jsonwebtoken": "~5.4.0", 14 | "morgan": "~1.3.0", 15 | "spartan": "file:../../../spartan-node/spartan-1.0.0.tgz" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /demo/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "1.0.0", 4 | "description": "Test client application for spartan", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "spartan" 11 | ], 12 | "author": "Binu Ramakrishnan ", 13 | "license": "BSD-2-Clause", 14 | "dependencies": { 15 | "request": "*", 16 | "spartan": "file:../../../spartan-node/spartan-1.0.0.tgz" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/server/README.md: -------------------------------------------------------------------------------- 1 | # Spartan Demo App Server 2 | 3 | # Test setup 4 | 5 | 1. clone the repo 6 | 2. cd server && npm install 7 | 3. Run the demo server. (Make sure you are already running spartan 8 | Attestation Service. Find README.md on the main page for install instructions) 9 | ``` 10 | $ node bin/www 11 | ``` 12 | 4. To test: cd ../client & nodejs index.js 13 | ``` 14 | $ cd ../client & nodejs index.js 15 | ``` 16 | 17 | NOTE: 18 | Make sure the Spartan server's public key is included in spartan APIs npm package. 19 | Clue: as_pubkey 20 | -------------------------------------------------------------------------------- /demo/server/config.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | // spartan demo server configuration file 8 | 9 | // listen port 10 | exports.port = 3001; 11 | 12 | exports.asPubKey = '../../server/keys/test-ES256-AS-pubkey.pem' 13 | exports.role = 'SuperRole' 14 | 15 | // ECDSA settings 16 | //exports.ecdsaKey='./pri.pem'; 17 | //exports.ecdsaCert='./pub.pem'; 18 | 19 | // options { 'prod', 'dev' } 20 | exports.environment = 'dev'; 21 | -------------------------------------------------------------------------------- /tests/auth.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | var auth = require('../routes/auth'); 7 | 8 | 9 | console.log(auth.hashGen('AABBCCEEDDFF')); 10 | var hash = auth.hashGen('AABBCCEEDDFF'); 11 | 12 | // Load hash from your password DB. 13 | if (auth.hashCompare('AABBCCEEDDFF', hash)) { 14 | console.log('pass'); 15 | } else { 16 | console.log('fail'); 17 | } 18 | 19 | // Load hash from your password DB. 20 | if (auth.hashCompare('AABBCCEEDDFF_123', hash)) { 21 | console.log('pass'); 22 | } else { 23 | console.log('fail'); 24 | } -------------------------------------------------------------------------------- /server/models/user.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var User = sequelize.define("User", { 10 | userid: { 11 | type: DataTypes.STRING(128), 12 | unique: true, 13 | allowNull: false 14 | }, 15 | type: DataTypes.STRING(32), 16 | userkey: DataTypes.STRING(4096), 17 | name: DataTypes.STRING(128), 18 | createdBy: DataTypes.STRING(128), 19 | role: DataTypes.STRING(32), 20 | domain: DataTypes.STRING(32), 21 | }); 22 | 23 | return User; 24 | }; -------------------------------------------------------------------------------- /demo/server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var debug = require('debug')('keyserver'); 3 | var app = require('../app'); 4 | var config = require('../config.js'); 5 | var https = require('https'); 6 | var fs = require('fs'); 7 | 8 | var port = process.env.PORT || config.port; 9 | 10 | if (config.tls === 1) { 11 | var options = { 12 | key: fs.readFileSync(config.tlsKey), 13 | cert: fs.readFileSync(config.tlsCert), 14 | }; 15 | 16 | var server = https.createServer(options, app).listen(port, function(){ 17 | console.log('HTTPS keyserver listening on port ' + server.address().port); 18 | }); 19 | 20 | } else { 21 | var server = app.listen(port, function() { 22 | console.log('HTTP keyserver listening on port ' + server.address().port); 23 | }); 24 | } 25 | 26 | -------------------------------------------------------------------------------- /demo/server/routes/router-utils.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | 8 | module.exports = { 9 | 10 | sendSuccessResponse: function (res, obj, code) { 11 | res.set({ 12 | 'Content-Type': 'application/json;charset=utf-8' 13 | }) 14 | .status(code || 200) 15 | .send({ 16 | 'msg': obj.msg || 'OK' 17 | }); 18 | }, 19 | sendSuccessResponseWObj: function (res, obj, code) { 20 | res.set({ 21 | 'Content-Type': 'application/json;charset=utf-8' 22 | }) 23 | .status(code || 200) 24 | .send(obj); 25 | }, 26 | sendErrorResponse: function (res, obj, code) { 27 | res.set({ 28 | 'Content-Type': 'application/json;charset=utf-8' 29 | }) 30 | .status(code || 400) 31 | .send({ 32 | 'msg': obj.msg || 'Bad Request' 33 | }); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // 3 | // Copyright 2015, Yahoo Inc. 4 | // Copyrights licensed under the New BSD License. See the 5 | // accompanying LICENSE.txt file for terms. 6 | // 7 | var debug = require('debug')('spartan'); 8 | var app = require('../app'); 9 | var config = require('../config.js'); 10 | var https = require('https'); 11 | var fs = require('fs'); 12 | 13 | var port = process.env.PORT || config.port; 14 | var host = process.env.HOST || config.host || '0.0.0.0'; 15 | 16 | if (config.tls === 1) { 17 | var options = { 18 | key: fs.readFileSync(config.tlsKey), 19 | cert: fs.readFileSync(config.tlsCert) 20 | }; 21 | 22 | var server = https.createServer(options, app).listen(port, host, function(){ 23 | console.log('HTTPS spartan server listening on port ' + server.address().port); 24 | }); 25 | 26 | } else { 27 | var server = app.listen(port, function() { 28 | console.log('HTTP spartan server listening on port ' + server.address().port); 29 | }); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /server/models/user-group.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var UserGroup = sequelize.define("UserGroup", { 10 | name: { 11 | type: DataTypes.STRING(128), 12 | unique: true, 13 | allowNull: false 14 | }, 15 | description: DataTypes.STRING(512), 16 | }, { 17 | classMethods: { 18 | associate: function(models) { 19 | UserGroup.belongsTo(models.User, { 20 | foreignKey: 'createdBy', 21 | targetKey: 'userid' 22 | }); 23 | UserGroup.belongsTo(models.User, { 24 | foreignKey: 'updatedBy', 25 | targetKey: 'userid' 26 | }); 27 | } 28 | } 29 | }); 30 | 31 | return UserGroup; 32 | }; -------------------------------------------------------------------------------- /server/models/app-in-role.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var AppInRole = sequelize.define("AppInRole", { 10 | attribute: DataTypes.STRING(32), 11 | }, { 12 | classMethods: { 13 | associate: function(models) { 14 | AppInRole.belongsTo(models.User, { 15 | foreignKey: 'createdBy', 16 | targetKey: 'userid' 17 | }); 18 | AppInRole.belongsTo(models.App, { 19 | foreignKey: 'appName', 20 | targetKey: 'name' 21 | }); 22 | AppInRole.belongsTo(models.Role, { 23 | foreignKey: 'roleName', 24 | targetKey: 'name' 25 | }); 26 | } 27 | } 28 | }); 29 | 30 | 31 | 32 | return AppInRole; 33 | }; -------------------------------------------------------------------------------- /server/models/user-in-group.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var UserInGroup = sequelize.define("UserInGroup", { 10 | userType: DataTypes.STRING(128), 11 | role: DataTypes.STRING(32), 12 | }, { 13 | classMethods: { 14 | associate: function(models) { 15 | UserInGroup.belongsTo(models.UserGroup, { 16 | foreignKey: 'userGroupName', 17 | targetKey: 'name' 18 | }); 19 | UserInGroup.belongsTo(models.User, { 20 | foreignKey: 'userid', 21 | targetKey: 'userid' 22 | }); 23 | UserInGroup.belongsTo(models.User, { 24 | foreignKey: 'createdBy', 25 | targetKey: 'userid' 26 | }); 27 | } 28 | } 29 | }); 30 | 31 | 32 | return UserInGroup; 33 | }; -------------------------------------------------------------------------------- /server/models/app.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var App = sequelize.define("App", { 10 | name: { 11 | type: DataTypes.STRING(128), 12 | unique: true, 13 | allowNull: false 14 | }, 15 | description: DataTypes.STRING(512), 16 | }, { 17 | classMethods: { 18 | associate: function(models) { 19 | App.belongsTo(models.User, { 20 | foreignKey: 'createdBy', 21 | targetKey: 'userid' 22 | }); 23 | App.belongsTo(models.User, { 24 | foreignKey: 'updatedBy', 25 | targetKey: 'userid' 26 | }); 27 | App.belongsTo(models.UserGroup, { 28 | foreignKey: 'ownedByUserGroup', 29 | targetKey: 'name' 30 | }); 31 | } 32 | } 33 | }); 34 | 35 | return App; 36 | }; -------------------------------------------------------------------------------- /server/models/member-in-app.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var MemberInApp = sequelize.define("MemberInApp", { 10 | identity: { 11 | type: DataTypes.STRING(512), 12 | unique: false, 13 | allowNull: false 14 | }, 15 | identityType: { 16 | type: DataTypes.TEXT 17 | }, 18 | role: DataTypes.STRING(32), 19 | expiry: { 20 | type: DataTypes.DATE 21 | }, 22 | }, { 23 | classMethods: { 24 | associate: function(models) { 25 | MemberInApp.belongsTo(models.App, { 26 | foreignKey: 'appName', 27 | targetKey: 'name' 28 | }); 29 | MemberInApp.belongsTo(models.User, { 30 | foreignKey: 'createdBy', 31 | targetKey: 'userid' 32 | }); 33 | } 34 | } 35 | }); 36 | 37 | return MemberInApp; 38 | }; 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spartan-server", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "start": "node ./server/bin/www", 6 | "test": "echo \"TODO: Add tests here.\" && exit 0" 7 | }, 8 | "bin": { 9 | "spartan-server": "./server/bin/www" 10 | }, 11 | "keywords": [ 12 | "spartan", 13 | "authorization", 14 | "authentication", 15 | "auth", 16 | "container" 17 | ], 18 | "author": { 19 | "name": "Binu Ramakrishnan" 20 | }, 21 | "contributors": [ 22 | {"name": "Aditya Mahendrakar"}, 23 | {"name": "Binu Ramakrishnan"} 24 | ], 25 | "bugs": { 26 | "url": "https://github.com/yahoo/spartan/issues" 27 | }, 28 | "license": "BSD-3-Clause", 29 | "dependencies": { 30 | "bcrypt": "^0.8.5", 31 | "body-parser": "~1.14.1", 32 | "cookie-parser": "~1.4.0", 33 | "debug": "~2.2.0", 34 | "express": "~4.13.3", 35 | "jsonwebtoken": "~5.4.1", 36 | "morgan": "~1.6.1", 37 | "spartan-api": "~0", 38 | "mysql": "^2.9.0", 39 | "request": "^2.65.0", 40 | "sequelize": "~3.14.1" 41 | }, 42 | "description": "Spartan - A Scalable Client Authentication & Authorization System for Container-based Environments" 43 | } 44 | -------------------------------------------------------------------------------- /server/models/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | var fs = require("fs"); 9 | var path = require("path"); 10 | var Sequelize = require("sequelize"); 11 | var config = require('../config.js'); 12 | var sequelize = new Sequelize(config.db_name, config.db_user, config.db_passwd, { 13 | host: config.db_host, 14 | logging: console.log, 15 | dialect: config.db_dialect, 16 | omitNull: true, 17 | pool: { 18 | max: 5, 19 | min: 0, 20 | idle: 10000 21 | } 22 | }); 23 | 24 | 25 | var db = {}; 26 | 27 | fs 28 | .readdirSync(__dirname) 29 | .filter(function(file) { 30 | return (file.indexOf(".") !== 0) && (file !== "index.js"); 31 | }) 32 | .forEach(function(file) { 33 | var model = sequelize.import(path.join(__dirname, file)); 34 | db[model.name] = model; 35 | }); 36 | 37 | Object.keys(db).forEach(function(modelName) { 38 | if ("associate" in db[modelName]) { 39 | db[modelName].associate(db); 40 | } 41 | }); 42 | 43 | db.sequelize = sequelize; 44 | db.Sequelize = Sequelize; 45 | 46 | module.exports = db; -------------------------------------------------------------------------------- /server/routes/router-utils.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | module.exports = { 7 | 8 | sendSuccessResponse: function (res, obj, code) { 9 | res.set({ 10 | 'Content-Type': 'application/json;charset=utf-8' 11 | }) 12 | .status(code || 200) 13 | .send({ 14 | 'msg': obj.msg || 'OK' 15 | }); 16 | }, 17 | sendSuccessResponseWObj: function (res, obj, code) { 18 | res.set({ 19 | 'Content-Type': 'application/json;charset=utf-8' 20 | }) 21 | .status(code || 200) 22 | .send(obj); 23 | }, 24 | sendSuccessResponseJSON: function (res, obj, code) { 25 | res.set({ 26 | 'Content-Type': 'application/json;charset=utf-8' 27 | }) 28 | .status(code || 200) 29 | .send(obj); 30 | }, 31 | 32 | sendSuccessResponseText: function (res, text, code) { 33 | res.set({ 34 | 'Content-Type': 'text/plain' 35 | }) 36 | .status(code || 200) 37 | .send(text); 38 | }, 39 | sendErrorResponse: function (res, obj, code) { 40 | res.set({ 41 | 'Content-Type': 'application/json;charset=utf-8' 42 | }) 43 | .status(code || 400) 44 | .send({ 45 | 'msg': obj.msg || 'Bad Request' 46 | }); 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /server/models/role.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | "use strict"; 7 | 8 | module.exports = function(sequelize, DataTypes) { 9 | var Role = sequelize.define("Role", { 10 | name: { 11 | type: DataTypes.STRING(128), 12 | unique: true, 13 | allowNull: false 14 | }, 15 | roleType: DataTypes.STRING(128), 16 | roleHandle: DataTypes.STRING(128), 17 | description: DataTypes.STRING(512), 18 | }, { 19 | classMethods: { 20 | associate: function(models) { 21 | Role.belongsTo(models.User, { 22 | foreignKey: 'createdBy', 23 | targetKey: 'userid' 24 | }); 25 | Role.belongsTo(models.User, { 26 | foreignKey: 'updatedBy', 27 | targetKey: 'userid' 28 | }); 29 | Role.belongsTo(models.UserGroup, { 30 | foreignKey: 'ownedByUserGroup', 31 | targetKey: 'name' 32 | }); 33 | Role.belongsTo(models.App, { 34 | foreignKey: 'ownedByApp', 35 | targetKey: 'name' 36 | }); 37 | } 38 | } 39 | }); 40 | 41 | return Role; 42 | }; -------------------------------------------------------------------------------- /server/bin/www-admin: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // 3 | // Copyright 2015, Yahoo Inc. 4 | // Copyrights licensed under the New BSD License. See the 5 | // accompanying LICENSE.txt file for terms. 6 | // 7 | 8 | "use strict"; 9 | var debug = require('debug')('spartan'); 10 | var adminApp = require('../app-admin'); 11 | var config = require('../config.js'); 12 | var https = require('https'); 13 | var fs = require('fs'); 14 | 15 | var adminPort = config.adminPort, 16 | localhost = "127.0.0.1"; 17 | 18 | if (config.tls === 1) { 19 | var options = { 20 | key: fs.readFileSync(config.adminTlsKey), 21 | cert: fs.readFileSync(config.adminTlsCert) 22 | }; 23 | 24 | var adminServer = https.createServer(optionsInternal, adminApp) 25 | .listen(adminPort, localhost, function() { 26 | console.log('Internal HTTPS spartan server listening on localhost port ' 27 | + adminServer.address().port); 28 | console.log('IMPORTANT : This is an admin interface only for adding new user accounts'); 29 | console.log('IMPORTANT : Shut it down immedietely after you add/remove users'); 30 | }); 31 | 32 | } else { 33 | var adminServer = adminApp.listen(adminPort, function() { 34 | console.log('HTTP spartan admin server listening on localhost port ' 35 | + adminServer.address().port); 36 | console.log('IMPORTANT : This is an admin interface for only adding new user accounts'); 37 | console.log('IMPORTANT : Shut it down immedietely after you add/remove users'); 38 | }); 39 | } 40 | 41 | -------------------------------------------------------------------------------- /doc/spartan-cli.markdown: -------------------------------------------------------------------------------- 1 | # spartan commandline utility 2 | spartan commandline utility can be used to interact with the provisioner service. 3 | Options and commands currently supported are listed below. 4 | 5 | ``` 6 | $ spartan help 7 | NAME 8 | spartan - commandline utility to interact with spartan 9 | 10 | SYNOPSIS 11 | spartan [flags] command [params] 12 | 13 | OPTIONS 14 | -u userid to be used 15 | -s base spartan URL to be used 16 | -c CA crt bundle path, if not default 17 | -v verbose 18 | 19 | 20 | STANDARD COMMANDS 21 | 22 | Usergroup commands 23 | 24 | show-usergroup 25 | create-usergroup [description ...] 26 | remove-usergroup 27 | add-to-usergroup [ ] 28 | remove-from-usergroup 29 | list-usergroups 30 | 31 | App group commands 32 | 33 | show-app 34 | create-app [description ...] 35 | remove-app 36 | add-to-app [ ] 37 | remove-from-app 38 | list-apps 39 | 40 | Role commands 41 | 42 | show-role 43 | create-role [ [description ...]] 44 | remove-role 45 | add-to-role 46 | remove-from-role 47 | list-roles 48 | 49 | type 'spartan help' to see all available commands 50 | type 'spartan help [command]' for usage of the specified command 51 | ``` 52 | -------------------------------------------------------------------------------- /doc/architecture.md: -------------------------------------------------------------------------------- 1 | # Spartan Architecture 2 | 3 | 4 | ### End to end flow 5 | 6 | 7 | 8 | Spartan consists 3 components: 9 | 10 | 1. A server component that is comprised of two services: 11 | 12 | * *Provisioner service* 13 | * *Attestation service* 14 | 15 | 2. Command line tool - Used by a user to provision applications with provisioner service 16 | 3. Client library. Provides language based APIs to your applications. Currently we have NodeJS and Go language bindings for spartan. The client APIs are used by client and server to fetch and validate AS tokens 17 | 18 | **Provsioner service** 19 | The provisioner service provides REST APIs for the user to provision user groups, application groups, roles and group membership 20 | 21 | The following entities (database tables) are part of provisioner service: 22 | 23 | * *User Group* - A group of users. Each user has a role (e.g. Admin). 24 | * *Apps* - Your application is represented as `Apps`. If your application has more than one instances (more servers), you may group it as one entity using Apps. 25 | * *Roles* - Represents a privilege to access a resource. If your app wanted to access a role protected resource, then add the apps as a member to that role 26 | * *Users* - User database. If you already have an identity system that issues a JWT/OpenID Connect token, then you may not need this 27 | 28 | Interface to these APIs are through the `spartan` command line tool. 29 | 30 | **Attestation service** 31 | The attestation service provides APIs that issues certificate tokens after verifying relationship between application and role 32 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4.1' 4 | - '4.0' 5 | - '0.12' 6 | - '0.11' 7 | - '0.10' 8 | 9 | env: 10 | - CXX=g++-4.8 11 | 12 | notifications: 13 | email: 14 | recipients: 15 | - a.mahendrakar@yahoo.com 16 | on_success: change 17 | on_failure: always 18 | after_success: 19 | - test $(cat $TRAVIS_BUILD_DIR/package.json | grep version | awk '{print $2}' | sed 's/"//g' | sed 's/,//g' | awk '{print "v"$1}' ) = $TRAVIS_TAG && test $(echo $TRAVIS_NODE_VERSION | awk '{print $1}' ) = '0.12' && export VALID_VERSION=true 20 | deploy: 21 | provider: npm 22 | email: a.mahendrakar@yahoo.com 23 | api_key: 24 | secure: vtgw1h09vxTfht4Ybo8xct0hIxJ99qw4g1ug4EW2+hrOmHD0jvnwNomA2wq74ErnNDJpadji/96q7jjiredrCFtXJr+jJfgWvz9Dj/3DYka5BLZf3mXt4sSZsIa8a4qd4yjps8LSTz8Zn+NdamBmJZF/Tyw1p/KqijpQ8uUt2sN8Oil9Fgb4fZ5cq5Xhv431VqjHD0Q21HB2GDgEFJxCvy5tXCKOfmTwzox/fwf9gTyrkYB27v9UTGxdqJqHrVuPRuaqbOuGIHNfRLc8qZAbtjp/DQmvf/itsI49Pxkm9LERCCAxVum60EEm79VYjSqnpC05ybYW1XjMrScY9Sqz2VBfftZa1bj7awqHw/K2aDmmSjzc7CUK7aJ1pQVy+xmdp9Je+aHqMlYvzy2HLLQYt4SZSn3zaTQFl2x3tkyivaY5i/b/Rf5BasaNxm2fPYI0QVyOZBXojPPULiHqyDdLrMKOpT13n7olPWbTkwwuI++eixP2OqJiSRlMtn3L4Y27x028LgJJaecueVhtrGnC7+9CClod6V4nJvbS2oTWRsP5s8O4A0vfCVGd6fhd7MhOVjR9Wz6jMxKkMrylWYEB6U5lqauQykYVXHl4ndnh/8vTzHWYCKzOm6kd60qQ0DPEzHfFPCVXYN2tPYzML1sFzNl1kCv0kBtTk705cTO3heA= 25 | on: 26 | condition: $VALID_VERSION = true 27 | tags: true 28 | branch: master 29 | sudo: false 30 | 31 | addons: 32 | apt: 33 | sources: 34 | - ubuntu-toolchain-r-test 35 | packages: 36 | - g++-4.8 37 | 38 | before_install: 39 | - $CXX --version 40 | - if [ "$TRAVIS_NODE_VERSION" = "0.8" ]; then npm install -g npm@2.7.3; fi; 41 | -------------------------------------------------------------------------------- /demo/server/app.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Author: Binu Ramakrishnan 7 | // Created: 11/01/2015 8 | 9 | "use strict"; 10 | var express = require('express'); 11 | var logger = require('morgan'); 12 | var cookieParser = require('cookie-parser'); 13 | var bodyParser = require('body-parser'); 14 | var config = require('./config.js'); 15 | 16 | var svc = require('./routes/service-auth'); 17 | 18 | var app = express(); 19 | 20 | // uncomment after placing your favicon in /public 21 | //app.use(favicon(__dirname + '/public/favicon.ico')); 22 | app.use(logger('dev')); 23 | app.use(bodyParser.json()); 24 | app.use(bodyParser.urlencoded({ 25 | extended: false 26 | })); 27 | app.use(cookieParser()); 28 | 29 | app.use('/v1/service', svc); 30 | 31 | // catch 404 and forward to error handler 32 | app.use(function (req, res, next) { 33 | var err = new Error('Not Found'); 34 | err.status = 404; 35 | next(err); 36 | }); 37 | 38 | // error handlers 39 | 40 | // development error handler 41 | // will print stacktrace 42 | if (config.environment === 'dev') { 43 | app.use(function (err, req, res, next) { 44 | res.status(err.status || 500) 45 | .send('error', { 46 | message: err.message, 47 | error: err 48 | }); 49 | }); 50 | } 51 | 52 | // production error handler 53 | // no stacktraces leaked to user 54 | app.use(function (err, req, res, next) { 55 | res.status(err.status || 500) 56 | .send('error', { 57 | message: err.message, 58 | error: {} 59 | }); 60 | }); 61 | 62 | module.exports = app; 63 | -------------------------------------------------------------------------------- /demo/client/ecdh-test.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Author: Binu Ramakrishnan 7 | // Created: 11/01/2015 8 | 9 | "use strict"; 10 | var fs = require('fs'); 11 | var spartan = require('spartan'); 12 | var SpartanECDH = require('spartan/ecdh'); 13 | var request = require('request'); 14 | var svc_uri = 'http://localhost:3001/v1/service/ecdh'; 15 | 16 | spartan.getToken('SuperRole', { 17 | app_privkey: fs.readFileSync('./keys/test-ES256-app-privkey.pem'), 18 | app_pubkey: fs.readFileSync('./keys/test-ES256-app-pubkey.pem', 'utf8'), 19 | // as_pubkey: null, //fs.readFileSync('./as-public-key.pem'), 20 | as_url: 'http://localhost:3000/v1/as/tokens' 21 | }, 22 | function (error, certs) { 23 | 24 | if (error) { 25 | console.error( 26 | 'Error: failed to return certs from Attestation Service: ' + error); 27 | return; 28 | } 29 | 30 | var ecdh = new SpartanECDH(), 31 | options = { 32 | uri: svc_uri, 33 | method: 'POST', 34 | json: { 35 | spartantoken: certs, 36 | public_key: ecdh.getPublicKey() 37 | } 38 | }; 39 | 40 | request(options, function (error, response, body) { 41 | if (error) { 42 | console.error('Error: service access error:', error); 43 | return; 44 | } 45 | 46 | if (response.statusCode !== 200) { 47 | console.error(body); 48 | return; 49 | } 50 | 51 | console.log('Secret: ' + ecdh.getSharedSecret(body.public_key)); 52 | 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Software License Agreement (BSD License) 2 | ======================================== 3 | 4 | Copyright 2015, Yahoo Inc. All rights reserved. 5 | ---------------------------------------------------- 6 | 7 | Redistribution and use of this software in source and binary forms, 8 | with or without modification, are permitted provided that the following 9 | conditions are met: 10 | 11 | * Redistributions of source code must retain the above 12 | copyright notice, this list of conditions and the 13 | following disclaimer. 14 | * Redistributions in binary form must reproduce the above 15 | copyright notice, this list of conditions and the 16 | following disclaimer in the documentation and/or other 17 | materials provided with the distribution. 18 | * Neither the name of Yahoo Inc. nor the names of its 19 | contributors may be used to endorse or promote products 20 | derived from this software without specific prior 21 | written permission of Yahoo Inc. 22 | 23 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS 24 | IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 25 | TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 26 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 27 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 28 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 29 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 30 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 31 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 32 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 33 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 34 | 35 | 36 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // spartan configuration file 7 | // listen host 8 | exports.host = "" 9 | 10 | // listen ports 11 | exports.port = 3000; 12 | exports.adminPort = 2999; 13 | 14 | // TLS settings 15 | // 1 => TLS enabled, (highly recommended) 16 | // 0 => TLS disabled (no TLS), 17 | exports.tls = 0; 18 | exports.tlsKey = '/path/to/tls/private/key'; 19 | exports.tlsCert = '/path/to/tls/public/cert'; 20 | 21 | // admin is running on localhost interface,so this may not be required. 22 | exports.adminTlsKey = '/path/to/tls/private/key'; 23 | exports.adminTlsCert = '/path/to/tls/public/cert'; 24 | 25 | // database 26 | exports.db_dialect = 'mysql'; // possible values: mysql, mariadb, sqlite, postgres, mssql 27 | exports.db_host = '127.0.0.1'; 28 | exports.db_name = 'spartan'; 29 | exports.db_user = ''; 30 | exports.db_passwd = ''; 31 | 32 | // ECDSA settings 33 | // NOTE replace this keys when use in production. 34 | // This key as important as root CA signing key, so keep it safe! 35 | // TODO guildelines to secure private key 36 | exports.ecdsaPrivateKey = __dirname + '/keys/test-ES256-AS-privkey.pem'; 37 | exports.ecdsaPublicKey = __dirname + '/keys/test-ES256-AS-pubkey.pem'; 38 | 39 | // Identity Provider's (IP) public key. 40 | // If the IP is spartan, then provide private key 41 | exports.IPPrivateKey= __dirname + '/keys/test-ES256-AS-privkey.pem'; 42 | // Public key of the IP 43 | exports.IPPublicKey= __dirname + '/keys/test-ES256-AS-pubkey.pem'; 44 | 45 | // cert/token expiry - 24 hours 46 | exports.expiresIn = 86400; 47 | exports.algorithm = 'ES256'; 48 | 49 | // options { 'prod', 'dev' } 50 | exports.environment = 'prod'; 51 | -------------------------------------------------------------------------------- /server/app-admin.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | 8 | var express = require('express'); 9 | var path = require('path'); 10 | //var favicon = require('serve-favicon'); 11 | var logger = require('morgan'); 12 | var cookieParser = require('cookie-parser'); 13 | var bodyParser = require('body-parser'); 14 | var config = require('./config'); 15 | 16 | var routes = require('./routes/index'); 17 | var user = require('./routes/user'); 18 | 19 | 20 | // different app and listener for user management 21 | var app = express(); 22 | 23 | app.use(logger('dev')); 24 | app.use(bodyParser.json()); 25 | app.use(bodyParser.urlencoded({ 26 | extended: false 27 | })); 28 | app.use(cookieParser()); 29 | //app.use(express.static(path.join(__dirname, 'public'))); 30 | 31 | app.use('/', routes); 32 | app.use('/v1/user', user); 33 | 34 | app.set('views', path.join(__dirname, 'views')); 35 | app.set('view engine', 'jade'); 36 | 37 | // catch 404 and forward to error handler 38 | app.use(function (req, res, next) { 39 | var err = new Error('Not Found'); 40 | err.status = 404; 41 | next(err); 42 | }); 43 | 44 | // error handlers 45 | 46 | // development error handler 47 | // will print stacktrace 48 | if (config.environment === 'dev') { 49 | app.use(function (err, req, res, next) { 50 | res.status(err.status || 500); 51 | res.render('error', { 52 | message: err.message, 53 | error: err 54 | }); 55 | }); 56 | } 57 | 58 | // production error handler 59 | // no stacktraces leaked to user 60 | app.use(function (err, req, res, next) { 61 | res.status(err.status || 500); 62 | res.render('error', { 63 | message: err.message, 64 | error: {} 65 | }); 66 | }); 67 | 68 | module.exports = app; 69 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | 8 | var express = require('express'); 9 | var path = require('path'); 10 | //var favicon = require('serve-favicon'); 11 | var logger = require('morgan'); 12 | var cookieParser = require('cookie-parser'); 13 | var bodyParser = require('body-parser'); 14 | var config = require('./config'); 15 | 16 | var routes = require('./routes/index'); 17 | var user_group = require('./routes/user-group'); 18 | var app_group = require('./routes/app-group'); 19 | var role_group = require('./routes/role-group'); 20 | var att_svc = require('./routes/attestation-service'); 21 | 22 | var app = express(); 23 | 24 | app.use(logger('dev')); 25 | app.use(bodyParser.json()); 26 | app.use(bodyParser.urlencoded({ 27 | extended: false 28 | })); 29 | app.use(cookieParser()); 30 | //app.use(express.static(path.join(__dirname, 'public'))); 31 | 32 | app.use('/', routes); 33 | app.use('/v1/usergroup', user_group); 34 | app.use('/v1/app', app_group); 35 | app.use('/v1/role', role_group); 36 | app.use('/v1/as', att_svc); 37 | 38 | app.set('views', path.join(__dirname, 'views')); 39 | app.set('view engine', 'jade'); 40 | 41 | // catch 404 and forward to error handler 42 | app.use(function (req, res, next) { 43 | var err = new Error('Not Found'); 44 | err.status = 404; 45 | next(err); 46 | }); 47 | 48 | // error handlers 49 | 50 | // development error handler 51 | // will print stacktrace 52 | if (config.environment === 'dev') { 53 | app.use(function (err, req, res, next) { 54 | res.status(err.status || 500); 55 | res.render('error', { 56 | message: err.message, 57 | error: err 58 | }); 59 | }); 60 | } 61 | 62 | // production error handler 63 | // no stacktraces leaked to user 64 | app.use(function (err, req, res, next) { 65 | res.status(err.status || 500); 66 | res.render('error', { 67 | message: err.message, 68 | error: {} 69 | }); 70 | }); 71 | 72 | module.exports = app; 73 | -------------------------------------------------------------------------------- /server/setup-auto.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS `Apps` (`id` INTEGER auto_increment , `name` VARCHAR(128) , `description` VARCHAR(512), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `createdBy` VARCHAR(128), `updatedBy` VARCHAR(128), `ownedByUserGroup` VARCHAR(128), PRIMARY KEY (`id`, `name`)) ENGINE=InnoDB; 2 | SHOW INDEX FROM `Apps`; 3 | CREATE TABLE IF NOT EXISTS `AppInRoles` (`id` INTEGER NOT NULL auto_increment , `role` VARCHAR(32), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `createdBy` VARCHAR(128), `appName` VARCHAR(128), `roleName` VARCHAR(128), PRIMARY KEY (`id`)) ENGINE=InnoDB; 4 | SHOW INDEX FROM `AppInRoles`; 5 | CREATE TABLE IF NOT EXISTS `MemberInApps` (`id` INTEGER NOT NULL auto_increment , `identity` VARCHAR(4092), `identityType` VARCHAR(4092), `appName` VARCHAR(128), `createdBy` VARCHAR(128), `role` VARCHAR(32), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB; 6 | SHOW INDEX FROM `MemberInApps`; 7 | CREATE TABLE IF NOT EXISTS `Roles` (`id` INTEGER auto_increment , `name` VARCHAR(128) , `roleType` VARCHAR(128), `roleHandle` VARCHAR(128), `description` VARCHAR(512), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `createdBy` VARCHAR(128), `updatedBy` VARCHAR(128), `ownedByUserGroup` VARCHAR(128), PRIMARY KEY (`id`, `name`)) ENGINE=InnoDB; 8 | SHOW INDEX FROM `Roles`; 9 | CREATE TABLE IF NOT EXISTS `Users` (`id` INTEGER auto_increment , `userid` VARCHAR(128) , `type` VARCHAR(32), `userkey` VARCHAR(4096), `name` VARCHAR(128), `createdBy` VARCHAR(128), `role` VARCHAR(32), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`, `userid`)) ENGINE=InnoDB; 10 | SHOW INDEX FROM `Users`; 11 | CREATE TABLE IF NOT EXISTS `UserGroups` (`id` INTEGER auto_increment , `name` VARCHAR(128) , `description` VARCHAR(512), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `createdBy` VARCHAR(128), `updatedBy` VARCHAR(128), PRIMARY KEY (`id`, `name`)) ENGINE=InnoDB; 12 | SHOW INDEX FROM `UserGroups`; 13 | CREATE TABLE IF NOT EXISTS `UserInGroups` (`id` INTEGER NOT NULL auto_increment , `userType` VARCHAR(128), `role` VARCHAR(32), `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, `userGroupName` VARCHAR(128), `userid` VARCHAR(128), `createdBy` VARCHAR(128), PRIMARY KEY (`id`)) ENGINE=InnoDB; 14 | SHOW INDEX FROM `UserInGroups`; 15 | -------------------------------------------------------------------------------- /demo/client/auth-test.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | 8 | "use strict"; 9 | var fs = require('fs'); 10 | var spartan = require('spartan-api'); 11 | var SpartanECDH = require('spartan/ecdh'); 12 | var request = require('request'); 13 | var svc_url = 'http://localhost:3001/v1/service/auth-test'; 14 | 15 | var privkey = fs.readFileSync('./keys/test-ES256-app-privkey.pem'); 16 | 17 | var ret = spartan.tokenSign({ 18 | sub: 'test-subject', 19 | iss: 'self', 20 | exp: 60, // 1 minute 21 | alg: 'ES256' 22 | }, { test: 'test_data' }, privkey); 23 | 24 | if (ret.success) { 25 | console.log('failed'); 26 | // return; 27 | } 28 | 29 | var getCertCallback = function (error, certs) { 30 | console.log('Certs : ' + certs); 31 | 32 | if (error) { 33 | console.error('Error: failed to return certs from Attestation Service: ' + 34 | JSON.stringify(error)); 35 | return; 36 | } 37 | 38 | var options = { 39 | uri: svc_url, 40 | method: 'POST', 41 | headers: { 42 | 'x-spartan-auth-token': certs 43 | }, 44 | 45 | json: {} 46 | }; 47 | 48 | request(options, function (error, response, body) { 49 | if (error) { 50 | console.error('Error: service access error:', error); 51 | return; 52 | } 53 | 54 | if (response.statusCode !== 200) { 55 | console.error(response.statusCode + ' ' + JSON.stringify(body)); 56 | return; 57 | } 58 | 59 | var resp = body; 60 | console.log(resp); 61 | }); 62 | 63 | }; 64 | 65 | var mkdirSync = function (path) { 66 | try { 67 | //fs.mkdirSync(path, 0o700); 68 | fs.mkdirSync(path, parseInt('0700', 8)); 69 | } catch (e) { 70 | if (e.code !== 'EEXIST') { throw e; } 71 | } 72 | }; 73 | 74 | // not great, but ok for testing. For prod, use an app specific dir 75 | var home = process.env['HOME'], 76 | path = home + '/.spartan'; 77 | 78 | console.log('cache_path: ' + path); 79 | mkdirSync(path); 80 | 81 | spartan.getToken('SuperRole', { 82 | app_privkey: fs.readFileSync('./keys/test-ES256-app-privkey.pem'), 83 | app_pubkey: fs.readFileSync('./keys/test-ES256-app-pubkey.pem', 'utf8'), 84 | as_url: 'http://localhost:3000/v1/as/tokens', 85 | //token_type: 'app-svc-req' 86 | //token_type: 'as-app-token' 87 | cache_path: path 88 | }, getCertCallback); 89 | -------------------------------------------------------------------------------- /doc/attestation-apis.md: -------------------------------------------------------------------------------- 1 | ##Attestation Service APIs 2 | 3 | The attestation service exposes only two APIs. 4 | 5 | NOTE: This info is not required for operating Spartan. However if you wanted to write Spartan API bindings for an unsupported language, this would be useful. 6 | 7 | **Get app authorization cert tokens from Attestation Service** 8 | 9 | ---- 10 | 11 | * **URL:** `/v1/as/certs` 12 | 13 | * **Method:** `POST` 14 | 15 | * **URL Params** 16 | None 17 | 18 | * **Data Params** 19 | 20 | `{ token: '' }` // the exact token format is documented in jwt-tokens.md 21 | 22 | It is also possible to send the token using HTTP header : `x-spartan-auth-token` 23 | 24 | * **Success Response:** 25 | 26 | * **Code:** 200 OK
27 | * **Content-Type:** `application/json`
28 | **Content:** 29 | ```javascript 30 | { certs: [ { role: '' , cert: 'eyJ0eXAiOi...' }, 31 | { role: '' , cert: 'eyDfghJXAi...' }, ... 32 | ] 33 | } // the exact token format is documented in jwt-tokens.md 34 | ``` 35 | 36 | * **Error Response:** 37 | 38 | * **Code:** 404 NOT FOUND
39 | * **Content-Type:** `application/json`
40 | **Content:** `{ msg : "Invalid AppID" }` 41 | 42 | OR 43 | 44 | * **Code:** 401 UNAUTHORIZED
45 | * **Content-Type:** `application/json`
46 | **Content:** `{ msg : "Invalid token/auth failed" }` 47 | 48 | * **Sample Call:** 49 | 50 | The recommended way is to use a spartan library. Refer demo/client/auth-test.js 51 | 52 | **Get Attestation Service (AS) public key** 53 | ---- 54 | Returns AS public key in `text/plain` 55 | 56 | * **URL:** `/v1/as/publickey` 57 | 58 | * **Method:** `GET` 59 | 60 | * **URL Params** 61 | None 62 | 63 | * **Success Response:** 64 | 65 | * **Code:** 200 OK
66 | **Content-Type:** text/plain
67 | **Content:** `-----BEGIN PUBLIC KEY----- 68 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE53B0XxV+ZhLlDXE/YW9WFEmILLW0y5x+ 69 | 2d7aIW4m2KsNaJNL5IPV1Ct4mPUy9kUea5uaBGz57VKoi5A6i31ehIer5wmJvtQt 70 | IoiheqVmLoAIiQmYx9N11GdbECoRg2rL 71 | -----END PUBLIC KEY-----` 72 | 73 | 74 | * **Error Response:** 75 | 76 | * **Code:** 404 NOT FOUND
77 | **Content:** `{ msg : "Request resource not found" }` 78 | 79 | OR 80 | 81 | * **Code:** 401 UNAUTHORIZED
82 | **Content:** `{ msg : "Invalid token/auth failed" }` 83 | 84 | * **Sample Call:** 85 | 86 | `curl https:///v1/as/publickey` 87 | 88 | 89 | -------------------------------------------------------------------------------- /server/routes/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Author: Binu Ramakrishnan 7 | // Created: 11/01/2015 8 | 9 | "use strict"; 10 | var fs = require('fs'); 11 | var express = require('express'); 12 | var router = express.Router(); 13 | var jwt = require('jsonwebtoken'); 14 | var config = require('../config.js'); 15 | var models = require('../models'); 16 | var auth = require('./user-auth'); 17 | var spartan = require('spartan-api'); 18 | 19 | var crypto = require('crypto'); 20 | 21 | // identity provider public key. 22 | // TODO support more than one identity provider 23 | var IP_privkey = fs.readFileSync(config.IPPrivateKey); 24 | var IP_pubkey = fs.readFileSync(config.IPPublicKey); 25 | 26 | // exchange password to get a JWT token. This is ideally inmplemented by 27 | // as part of your single sign-on. Ideally this functionality should be 28 | // implemented by corp employee auth servers. To keep it easy for our users, 29 | // the auth is based on a sharet secret. 30 | // example: curl -X POST -d 'user=user@example.com&passwd=ABCDEFGHIJ' localhost:2999/v1/auth/token 31 | router.post('/v1/auth/token', function (req, res) { 32 | 33 | if ((!req.body.userid) || (!req.body.passwd)) { 34 | return res.status(400).json({ 35 | msg: 'invalid parameters passed', 36 | userid: req.body.userid 37 | }); 38 | } 39 | 40 | models.User.find({ 41 | attributes: ['userkey'], 42 | where: { 43 | userid: req.body.userid 44 | } 45 | }).then(function (user) { 46 | if (user && user.dataValues && auth.hashCompare(req.body.passwd, 47 | user.dataValues.userkey)) { 48 | var data = { 49 | ver: 1, 50 | type: 'user-token', 51 | ip: req.connection.remoteAddress 52 | }, 53 | resp = spartan.tokenSign({ 54 | sub: req.body.userid, 55 | iss: 'spartan-domain', 56 | exp: config.expiresIn, 57 | alg: config.algorithm 58 | }, data, IP_privkey); 59 | 60 | if (resp.success) { 61 | return res.json({ 62 | userid: req.body.userid, 63 | token: resp.token 64 | }); 65 | } 66 | 67 | return res.status(401).json({ 68 | msg: 'token creation failed', 69 | userid: req.body.userid 70 | }); 71 | 72 | } 73 | 74 | res.status(401).json({ 75 | msg: 'passwd is incorrect', 76 | userid: req.body.userid 77 | }); 78 | 79 | return; 80 | 81 | }); 82 | }); 83 | 84 | module.exports = router; 85 | -------------------------------------------------------------------------------- /doc/getting-started.markdown: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | To deploy spartan, the first step is to install spartan server. It provides provisioner and attestation services. Once the server is setup, use our command-line interface to provision applications with the spartan server. 4 | 5 | ## Setting up spartan server 6 | 7 | ### Pre-requisites 8 | * Node.js 9 | * Database - MySQL. Other dialects (Postgres, MariaDB or SQLite) are supported, but not tested. 10 | 11 | ### Installation 12 | 13 | [1] Start MySQL server 14 | 15 | [2] Create a database for spartan service. This would look like: 16 | ``` 17 | mysql> create database spartan; 18 | Query OK, 1 row affected (0.02 sec) 19 | ``` 20 | [3] Create a user, password for spartan application that can read/write to this database 21 | 22 | [4] Install spartan server node application on your host. 23 | ``` 24 | $ npm install spartan-server 25 | ``` 26 | [5] Generate an ECDSA keypair using openssl 27 | ``` 28 | $ openssl ecparam -name secp256r1 -genkey -out priv.key 29 | $ openssl ec -in priv.key -pubout -out pub.key 30 | ``` 31 | 32 | [6] Update your [config.js][] as needed. Specifically, host/ip, port, database credentials, 33 | ECDSA key paths, TLS cert and private key location. 34 | It is highly recommended to use TLS certificates signed by a public CA. 35 | 36 | [7] Create/update the latest schema on the database 37 | 38 | ``` 39 | $ node syncdb.js 40 | ``` 41 | 42 | [8] Spartan server provides a simple user management API. For testing you may create users using spartan user management APIs. 43 | 44 | If you already have an existing identity management solution that supports OpenID Connect/JWT, spartan server can be easily configured to integrate with it. 45 | 46 | (Optional) If you want to add any users to the system, you can do so by running an admin interface 47 | ``` 48 | $ node bin/www-admin 49 | ``` 50 | and run the following curl comand to add user 51 | ``` 52 | $ curl -X POST -d 'userid=&userkey=&createdBy=' localhost:2999/v1/user/create 53 | ``` 54 | This is assuming the spartan's user management API are running on localhost port 2999. 55 | 56 | [9] At this point you can start spartan server 57 | ``` 58 | node bin/www 59 | ``` 60 | 61 | ## Installing and running the CLI 62 | 63 | The CLI is written in Go. To install, just `go get` it 64 | ``` 65 | $ go get -u github.com/yahoo/spartan-go 66 | ```` 67 | The commands and options currently supported by the CLI are listed in [spartan CLI documentation](https://github.com/yahoo/spartan-go/blob/master/README.md) 68 | 69 | 70 | ## Sample App, Role, UserGroup provisioning 71 | 72 | Check out [sample-provision.sh](https://github.com/yahoo/spartan-go/blob/master/utils/spartan/sample-provision.sh) which demonstrates commands to provision and associate app, usergroup and roles 73 | 74 | [config.js]: ../src/config.js 75 | 76 | ## Testing using demo client and server 77 | 78 | We have included a [demo](../demo) nodejs based client and server for testing. 79 | 80 | **Demo Server** To make the demo server work, copy the attestation server public key to demo/server and update the demo/server/config.js `asPubKey` with path info. 81 | 82 | Now start the server 83 | 84 | ``` 85 | $ node bin/www 86 | ``` 87 | 88 | **Demo Client** The client already has a test key pair included under keys directory, hence creating a new key pair for this demo client is optional. 89 | ``` 90 | $ cd demo/client 91 | $ node auth-test.js 92 | { msg: 'app is authenticated!' } 93 | ``` 94 | 95 | -------------------------------------------------------------------------------- /demo/server/routes/service-auth.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Created: 11/01/2015 7 | 8 | "use strict"; 9 | var fs = require('fs'); 10 | var express = require('express'); 11 | var router = express.Router(); 12 | var spartan = require('spartan-api'); 13 | var SpartanECDH = require('spartan/ecdh'); 14 | var config = require('../config'); 15 | var utils = require('./router-utils'); 16 | 17 | // Attestation server public key. 18 | // TODO: Need to support multiple public keys and versioning 19 | var as_pubkey = fs.readFileSync(config.asPubKey, 'utf8'); 20 | 21 | // Parameters to pass for auth 22 | var sp_options = { 23 | as_pubkey: as_pubkey, 24 | role: config.role, 25 | token_type: 'app-svc-req' // other options is 'as-app-token' 26 | }; 27 | 28 | var sp_handlr = new spartan.RouteHandler(sp_options); 29 | 30 | var roleAuthz = function (req, res, next) { 31 | if (req.authz_token.role === config.role) { 32 | console.log(req.authz_token.sub + ' is authorized to access ' + req.authz_token 33 | .role); 34 | next(); 35 | } else { 36 | return res.status(401).json({ 37 | msg: 'app is not authorized to acccess this resource' 38 | }); 39 | } 40 | }; 41 | 42 | // This is sample on how to authenticate client using express route handler 43 | router.post('/auth-test', [sp_handlr.svcAuth.bind(sp_handlr), roleAuthz], 44 | function (req, res) { 45 | 46 | // If you reach here, that means you are authorized to access this endpoint 47 | return res.status(200).json({ 48 | msg: 'app is authenticated!' 49 | }); 50 | }); 51 | 52 | // This is sample on how to authenticate client without 53 | // using express route handler 54 | router.post('/auth-test2', function (req, res) { 55 | 56 | // check header or url parameters or post parameters for token 57 | var token = req.body.spartantoken || 58 | req.query.spartantoken || 59 | req.headers['x-spartan-auth-token'], 60 | options = sp_options, 61 | ret; 62 | options.remote_ip = req.connection.remoteAddress; 63 | 64 | ret = spartan.tokenAuth(token, options); 65 | 66 | if (ret.success) { 67 | // In case you want, or you can directly access decoded token from ret.data 68 | /*if (ret.data.auth_token) { 69 | req.auth_token = ret.data.auth_token; 70 | } 71 | 72 | if (ret.data.authz_token) { 73 | req.authz_token = ret.data.authz_token; 74 | }*/ 75 | 76 | // you are now authenticated and authorized! 77 | return res.status(200).json({ 78 | msg: 'app is authenticated!' 79 | }); 80 | 81 | } 82 | 83 | return utils.sendErrorResponse(res, { 84 | msg: ret.msg 85 | }, ret.return_code); 86 | 87 | }); 88 | 89 | var sp_ecdh_handlr = new spartan.RouteHandler({ 90 | as_pubkey: as_pubkey, 91 | role: 'SuperRole', 92 | }); 93 | 94 | // TODO experimental, not ready for production yet! 95 | router.post('/ecdh', [sp_ecdh_handlr.svcAuth.bind(sp_ecdh_handlr)], 96 | function (req, res) { 97 | 98 | var ecdh = new SpartanECDH(), 99 | secret = ecdh.getSharedSecret(req.body.public_key); 100 | 101 | console.log('Secret: ' + secret); 102 | 103 | // TODO add mutual auth support 104 | return res.status(200).json({ 105 | public_key: ecdh.getPublicKey() 106 | }); 107 | }); 108 | 109 | module.exports = router; 110 | -------------------------------------------------------------------------------- /doc/jwt-tokens.md: -------------------------------------------------------------------------------- 1 | 2 | ## Spartan JWT Types 3 | 4 | Spartan heavily uses JSON Web Tokens (JWT) for authentication and authorization. It currently supports four types of tokens: 5 | 6 | 1. *User token*: represents a user. This token is issued by an identity provider and is compatible with OpenID Connect tokens 7 | 2. *AS app request token*: A self signed token from application requesting an authz token from Attestation Service (AS). 8 | 3. *AS app response token*: A token issued by AS for authorization (in reponse to the previous request to AS) 9 | 4. *Service app request token*: A self signed token from client application, wrapping AS token inside, requesting a protected resource. For requests over HTTPS, directly sending authz token (with out self-signed wrapper token) is ok. 10 | 11 | Spartan tokens are passed to the other end using a separate HTTP header - `x-spartan-auth-token`. Since spartan tokens are platform agnostic, it can be used with any client-server protocol. 12 | 13 | **NOTE**: This info is not required for operating Spartan. However if you wanted to write Spartan API bindings for an unsupported language, this would be useful. 14 | 15 | --- 16 | 17 | **User token** 18 | 19 | This is the decoded JWT struct. JWT are base64 encoded. User token is issued in exchange to a username and password. This is mostly used for accessing provisioner server APIs. The recomended practice is to use your identity server to issue a JWT token with the format as below: 20 | 21 | It is also possible to use OpenID Connect tokens as user token. Refer [Consuming a google user token](http://ncona.com/2015/02/consuming-a-google-id-token-from-a-server/) to learn about using identites from external providers. 22 | 23 | 24 | ```javascript 25 | header: { 26 | alg: 'ES256', 27 | typ: 'JWT' 28 | } 29 | 30 | payload: { 31 | iat: 1446014735, // issued at timestamp 32 | exp: 1446018335, // token expiry timestamp 33 | ver: 1, 34 | type: 'user-token', 35 | sub: '', 36 | iss: '' // default: spartan-domain if the user is in Users table 37 | } 38 | 39 | signature: { } 40 | ``` 41 | 42 | --- 43 | 44 | **Application AS request token** 45 | 46 | ```javascript 47 | header: { 48 | alg: 'ES256', 49 | typ: 'JWT' 50 | } 51 | 52 | payload: { 53 | iat: 1446014735, // issued at timestamp 54 | exp: 1446018335, // token expiry timestamp 55 | ver: 1, 56 | type: 'as-app-req', 57 | sub: '', 58 | iss: 'self', 59 | pubkey: '', 60 | role: '', 61 | nonce: '<64-bit random number in hex>' 62 | } 63 | 64 | signature: { } 65 | ``` 66 | 67 | --- 68 | 69 | **Attestation Service response token (astoken)** 70 | 71 | ```javascript 72 | header: { 73 | alg: 'ES256', 74 | typ: 'JWT' 75 | } 76 | 77 | payload: { 78 | iat: 1446014735, // issued at timestamp 79 | exp: 1446018335, // token expiry timestamp 80 | ver: 1; 81 | type: 'as-app-token', 82 | sub: '', 83 | iss : 'spartan-domain', 84 | role: '', 85 | ip: '' 86 | } 87 | 88 | signature: { } 89 | ``` 90 | 91 | --- 92 | 93 | **Application's service request token** 94 | 95 | ```javascript 96 | header: { 97 | alg: 'ES256', 98 | typ: 'JWT' 99 | } 100 | 101 | payload: { 102 | iat: 1446014735, // issued at timestamp 103 | exp: 1446018335, // token expiry timestamp 104 | ver: 1, 105 | type: 'app-svc-req', 106 | sub: '', 107 | iss: 'self', 108 | pubkey: '', 109 | astoken: '', 110 | nonce: '<64-bit random number in hex>' 111 | } 112 | 113 | signature: { } 114 | ``` 115 | 116 | **TODO** 117 | 118 | Support user tokens for applications. The idea is to issue an AS authz token for a user in exchange of User Token. 119 | 120 | -------------------------------------------------------------------------------- /server/routes/attestation-service.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Author: Binu Ramakrishnan 7 | // Created: 11/01/2015 8 | 9 | "use strict"; 10 | 11 | var fs = require('fs'); 12 | var express = require('express'); 13 | var router = express.Router(); 14 | var jwt = require('jsonwebtoken'); 15 | var spartan = require('spartan-api'); 16 | var config = require('../config.js'); 17 | var models = require('../models'); 18 | var router_utils = require('./router-utils'); 19 | 20 | var as_privkey = fs.readFileSync(config.ecdsaPrivateKey); 21 | var as_pubkey = fs.readFileSync(config.ecdsaPublicKey, 'utf8'); 22 | 23 | var sp_handlr = new spartan.RouteHandler({ 24 | as_pubkey: as_pubkey 25 | }); 26 | 27 | /** 28 | * Return tokens for the requested app. The app is authenticated 29 | * with a token, self signed by the app. The token is passed 30 | * using HTTP header - x-spartan-auth-token 31 | * @returns {JSON} tokens - tokens for the app based of its role membership 32 | */ 33 | router.get('/tokens', [sp_handlr.asAuth.bind(sp_handlr)], function (req, res) { 34 | 35 | var query = 'select distinct roleName, attribute from AppInRoles where appName in' + 36 | ' (select appName from MemberInApps where identity= :identity )'; 37 | 38 | models.sequelize.query(query, { 39 | replacements: { 40 | identity: req.token.sub 41 | }, 42 | type: models.sequelize.QueryTypes.SELECT 43 | }).then(function (roles) { 44 | 45 | if (roles.length === 0) { 46 | return router_utils.sendErrorResponse(res, { 47 | 'msg': 'No roles found for the requested app' 48 | }, 404); 49 | } 50 | 51 | var tokens = { 52 | tokens: [] 53 | }, 54 | data, 55 | ret, 56 | i, 57 | j, 58 | cond; 59 | 60 | var uroles = []; 61 | 62 | for (i in roles) { 63 | cond = false; 64 | if (roles.hasOwnProperty(i)) { 65 | //console.log(roles[i].roleName); 66 | //console.log(roles[i].attribute); 67 | for (j = 0; j < uroles.length; j++) { 68 | if (uroles[j].role === roles[i].roleName) { 69 | uroles[j].attr.push(roles[i].attribute); 70 | cond = true; 71 | break; 72 | } 73 | } 74 | 75 | if (cond) { 76 | cond = false; 77 | continue; 78 | } 79 | 80 | data = { 81 | ver: 1, 82 | type: 'as-app-token', 83 | role: roles[i].roleName, 84 | attr: [ roles[i].attribute ], 85 | ip: req.connection.remoteAddress 86 | }; 87 | 88 | uroles.push(data); 89 | } 90 | } 91 | 92 | for (i in uroles) { 93 | if (uroles.hasOwnProperty(i)) { 94 | //console.log(uroles[i].role); 95 | console.log(JSON.stringify(uroles[i])); 96 | 97 | ret = spartan.tokenSign({ 98 | sub: req.token.sub, 99 | iss: 'spartan-domain', 100 | exp: config.expiresIn, //TODO: exp should be min(config.expiresIn, expiry of identity) 101 | algorithm: config.algorithm 102 | }, uroles[i], as_privkey); 103 | 104 | if (ret.success) { 105 | console.log(ret.token); 106 | tokens.tokens.push({ 107 | role: roles[i].roleName, 108 | astoken: ret.token 109 | }); 110 | } 111 | 112 | return router_utils.sendSuccessResponseJSON(res, tokens); 113 | } 114 | } 115 | 116 | console.log(JSON.stringify(tokens, null, 4)); 117 | }).catch(function (e) { 118 | console.error("unable to get record.."); 119 | console.error(e); 120 | return router_utils.sendErrorResponse(res, { 121 | 'msg': 'Unable to get appName' 122 | }, 500); 123 | }); 124 | 125 | }); 126 | 127 | /** 128 | * Attestation service public key 129 | * @returns text/plain public key 130 | */ 131 | router.get('/publickey', function (req, res) { 132 | return router_utils.sendSuccessResponseText(res, as_pubkey, 200); 133 | }); 134 | 135 | module.exports = router; 136 | -------------------------------------------------------------------------------- /server/routes/user-auth.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | // Author: Binu Ramakrishnan 7 | // Created: 11/01/2015 8 | 9 | "use strict"; 10 | var bcrypt = require('bcrypt'); 11 | var spartan = require('spartan-api'); 12 | var fs = require('fs'); 13 | var config = require('../config'); 14 | var utils = require('./router-utils'); 15 | var models = require('../models'); 16 | 17 | // Identity provider's public key 18 | var IP_pubkey = fs.readFileSync(config.IPPublicKey); 19 | 20 | module.exports = { 21 | 22 | hashGen: function (passwd) { 23 | // TODO error handling 24 | var salt = bcrypt.genSaltSync(10); 25 | var hash = bcrypt.hashSync(passwd, salt); 26 | // Store hash in your password DB. 27 | return hash; 28 | }, 29 | 30 | 31 | hashCompare: function (passwd, hash) { 32 | return bcrypt.compareSync(passwd, hash); 33 | }, 34 | 35 | // Use this function for APIs that are protected using JWT. 36 | // Example usage - check routes/ca-server.js 37 | verify: function (req, res, next) { 38 | // check header or url parameters or post parameters for token 39 | var token = req.body.token || req.query.token || 40 | req.headers['x-spartan-auth-token'], 41 | decoded; 42 | 43 | if (token) { 44 | decoded = spartan.tokenVerify(token, IP_pubkey); 45 | if (decoded.success === true) { 46 | req.token = decoded.data; 47 | next(); 48 | } else { 49 | return utils.sendErrorResponse(res, { 50 | 'msg': 'token verify failed' 51 | }, 403); 52 | } 53 | 54 | } else { 55 | return utils.sendErrorResponse(res, { 56 | 'msg': 'no token found' 57 | }, 403); 58 | } 59 | 60 | }, 61 | 62 | authzUserApp: function (req, res, next) { 63 | var requestor = req.token.sub, 64 | app = req.body.app || req.params.app; 65 | 66 | if (requestor && app) { 67 | models.sequelize.query( 68 | "SELECT name FROM Apps WHERE name= :app AND ownedByUserGroup in (select userGroupName from UserInGroups where userid= :userid)", { 69 | replacements: { 70 | app: app, 71 | userid: requestor 72 | }, 73 | type: models.sequelize.QueryTypes.SELECT 74 | }) 75 | .then(function (apps) { 76 | if (apps && apps.length > 0) { 77 | // user is authorized! 78 | next(); 79 | } else { 80 | return utils.sendErrorResponse(res, { 81 | 'msg': 'Not authorized to do this operation on the app' 82 | }, 403); 83 | } 84 | }) 85 | .catch(function (e) { 86 | console.error("unable to authorize request.."); 87 | console.error(e); 88 | return utils.sendErrorResponse(res, { 89 | 'msg': 'Unable to authorize request' 90 | }, 500); 91 | }); 92 | } else { 93 | return utils.sendErrorResponse(res, { 94 | 'msg': 'Mandatory field(s) missing' 95 | }); 96 | } 97 | }, 98 | 99 | authzUserRole: function (req, res, next) { 100 | var requestor = req.token.sub, 101 | role = req.body.role || req.params.role; 102 | 103 | if (requestor && role) { 104 | models.sequelize.query( 105 | "SELECT name FROM Roles WHERE name= :role AND ownedByUserGroup in (select userGroupName from UserInGroups where userid= :userid)", { 106 | replacements: { 107 | role: role, 108 | userid: requestor 109 | }, 110 | type: models.sequelize.QueryTypes.SELECT 111 | }) 112 | .then(function (roles) { 113 | if (roles && roles.length > 0) { 114 | // user is authorized! 115 | next(); 116 | } else { 117 | return utils.sendErrorResponse(res, { 118 | 'msg': 'Not authorized to do this operation on the role' 119 | }, 403); 120 | } 121 | }) 122 | .catch(function (e) { 123 | console.error("unable to authorize request.."); 124 | console.error(e); 125 | return utils.sendErrorResponse(res, { 126 | 'msg': 'Unable to authorize request' 127 | }, 500); 128 | }); 129 | } else { 130 | return utils.sendErrorResponse(res, { 131 | 'msg': 'Mandatory field(s) missing' 132 | }); 133 | } 134 | } 135 | 136 | }; 137 | -------------------------------------------------------------------------------- /doc/identity-bootstrapping.md: -------------------------------------------------------------------------------- 1 | ## Bootstrapping Service Identity in Platform as a Service (PaaS) Environments 2 | 3 | This document presents a secure architecture to bootstrap service identities in PaaS environments 4 | (eg. Kubernetes or Applications running on AWS). The model is abstracted 5 | out to make it generic as possible, and the following sequence diagram depicts the overall flow of the model. 6 | 7 | 8 | 9 | The above flow diagram is modeled based on OAuth2 flow. From an OAuth2 view, SpartanX is the identity provider, 10 | Tenant is the resource owner, and the Orchestrator (provider) is the 3rd party who deploys applications (resources) 11 | behalf of the tenant. 12 | 13 | In this model, we need a SpartanX service with a set of APIs exposed. These APIs are invoked by: 14 | 15 | 1. Tenant - to register and provision their applications 16 | 2. Orchestrator master node - to Add and Remove application identities 17 | 3. Worker node - supports application instances running on the node to fetch and refresh service identities and certificates. 18 | A worker node is a node under Orchestrator's domain on which application instances are launched. 19 | 20 | The Worker node is authenticated to SpartanX using Mutual TLS (mTLS). The master node during deployment binds application’s 21 | public key fingerprint with worker node’s identity and register it with SpartanX using `AddToTenantApp` API. Each application 22 | instance running on a worker node will have a process (sidecar proxy) responsible for refreshing the certificates for that 23 | application instance. The sidecar proxy at regular intervals connects to SpartanX through Node agent (not shown in the diagram). 24 | The Node agent forwards the sidecar proxy request to SpartanX and authenticates using mTLS. 25 | 26 | Note that Orchestrator is a trusted system, not a security system. As a good security practice, we should not overload Orchestrator 27 | to make it a trust anchor, instead security of the application deployed should be separated out from the Orchestrator. In this context, 28 | separation means the separation of the control. Decoupling is a desired property here. 29 | 30 | ### Implementation 31 | To implement this model, Orchestrator needs to expose hooks at various phases of application deploy lifecycle. Orchestrator is required 32 | to call the following APIs: 33 | 34 | `AuthorizeTenentAppAccess(tenant_atoken, provider_token, app_id)` 35 | 36 | The above API authorizes the Orchestrator (provider) to add and remove service identities to the given app id or a namespace. 37 | Tenant authorization is enabled through `tenant_atoken`, a bearer token delegated to the Orchestrator by the Tenant or Tenant's agent. 38 | Provider already have an existing relationship with SpartanX (not shown in the diagram), and the provider_token authenticates 39 | Provider to SpartanX. 40 | 41 | `AddToTenentApp(provider_token, app_id, public_key_fp, node_id)` 42 | 43 | Adds fingerprint of the public key generated inside the new instance to the associated app id. 44 | 45 | `RemoveFromTenentApp(provider_token, app_id, public_key_fp)` 46 | 47 | Remove the public_key fingerprint from the app id. This function is invoked by Orchestrator when the instance is killed or ceased to exist. 48 | 49 | `RevokeTenentAppAccess(provider_token, app_id)` 50 | 51 | Remove Orchestrator authorization on the given app id. 52 | 53 | `tenant_atoken`: Bearer token passed by tenant to the Orchestrator. Orchestrator API would be invoked directly by tenant SRE or 54 | through tenant’s CI/CD deployment job. `tenant_atoken` is used by Orchestrator to get authorization to deploy application behalf 55 | of the owner (See my tech talk [slides](https://www.slideshare.net/BinuRamakrishnan/securing-application-deployments-in-multitenant-cicd-environments) 56 | on _Securing Application Deployments in Multi-tenant CI/CD Environments_. 57 | 58 | `provider_token`: A token that identifies the orchestrator. 59 | 60 | `app_id`: Application id, recognized by SpartanX 61 | 62 | `node_id`: The identity of the worker node from which cert request is allowed for a given public key/app_id pair. 63 | Typically a worker node is authenticated through mutual TLS. The node id is CNAME/SAN of a X509 certificate used by 64 | Node agent for client authentication. 65 | 66 | ### Variations 67 | There are multiple ways to bootstrap service identity in a PAAS environment. An alternate option is where the SpartanX depends 68 | on Orchestrator to return `app_id`, `public_key_fp` and `node_id` bindings. In that case SpartanX is nothing but a proxy that queries 69 | Orchestrator and issues certificates based on the response. In such models, we may end up overly trusting Orchestrator. 70 | 71 | ### Notes 72 | An Orchestrator typically consists of a master node and a worker node agent. The agent runs on a worker node and their primary 73 | function is to help and support the master node to launch and manage instances running on that node. 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Spartan 2 | ## A Scalable Client Authentication & Authorization System for Container-based Environments 3 | 4 | **Please note:** This repo contains known security vulnerabilities. Use at your own risk! 5 | 6 | [Alpha release, not production ready] 7 | 8 | *USENIX UCMS 2015 Presentation slides: [here](https://www.slideshare.net/BinuRamakrishnan/a-scalable-client-authentication-authorization-service-for-containerbased-environments)* 9 | 10 | *An X509 based architecture called SpartanX is described [here](https://github.com/yahoo/spartan/blob/master/doc/spartanX.md)* 11 | 12 | [![npm version][npm-badge]][npm] 13 | [![dependency status][dep-badge]][dep-status] 14 | [![Build Status](https://travis-ci.org/yahoo/spartan.svg?branch=master)](https://travis-ci.org/yahoo/spartan) 15 | 16 | [npm]: https://www.npmjs.org/package/spartan-server 17 | [npm-badge]: https://img.shields.io/npm/v/spartan-server.svg?style=flat-square 18 | [dep-status]: https://david-dm.org/yahoo/spartan 19 | [dep-badge]: https://img.shields.io/david/yahoo/spartan.svg?style=flat-square 20 | 21 | 22 | 23 | 24 | ## Background 25 | Container technologies are revolutionizing the way we develop, build and deploy applications in large scale production environments. Applications running in containers often need to connect to various internal/external services that require authentication and authorization. Authenticating client application to a server is a challenge in such dynamic environments because we cannot rely on traditional IP or hostname based checks. IP based authentication no longer works because (1) container IP is dynamic and often repurposed (2) containers often share IPs. Alternate options include the use of TLS client certs and other key based authentication schemes. TLS client certificates provide authentication, but not authorization capabilities by its own and is not easy to configure and operate at scale - think about CICD pipeline spawning hundreds of containers that live only for few minutes! 26 | 27 | ## What is spartan 28 | Spartan is a role based identity system that provides both authentication and authorization to clients in an automated, easy to configure, scalable fashion. The system comprises of 29 | 30 | * Command line tools and APIs for node and application provisioners to manage and publish public key fingerprints 31 | * Provisioner service that provides grouping of public key fingerprints of nodes/applications to roles that represents a capability 32 | * Attestation service for the nodes & applications to get a signed tokens on demand that asserts the requested node's role membership. 33 | 34 | Your server application (service provider) maps the role with service specific capabilities and the requests are validated against the auth tokens placed by the client while making requests to the server. The system is designed from ground up based on our experience with an existing IP based authorization system, keeping practicality, flexibility and security in mind. The implementation makes use of modern security and crypto practices and such as ECDSA and JWT. 35 | 36 | ### Is spartan a replacement for TLS in my application ? 37 | Spartan is complimentary to TLS. Spartan's primary goal is to enable client authentication and authorization capabilities. However it can provide mutual authentication as well. TLS is recommended for server authentication and transport security. TLS for client authentication is possible but is hard to operationalize at scale, especially in dynamic environments. Authorization capabilities in TLS certificates is also limited, if not impossible. Spartan is light weight form of PKI that provides identity, authentication and authorization capabilities. Transport security is also possible with ECDHE key exchange. 38 | 39 | [**Update**] *An X509 based architecture called SpartanX is described [here](https://github.com/yahoo/spartan/blob/master/doc/spartanX.md)* 40 | 41 | ## Is this something for me? 42 | Spartan would be useful if you have experienced any of the following problems: 43 | 44 | * Enable fine granular access control for your application 45 | * You have a HTTPS service, but want to enable client authentication and authrorization capabilities 46 | * You are using client IP whitelists for access control, but find it less effective on shared IP environments like containers, NATs etc. 47 | * Spartan as an alternate to manual client IP whitelisting technique 48 | 49 | ## Features 50 | 51 | * No key management hassles. Private keys are not distributed, passed around or reused 52 | * Based on open standards - JSON Web Tokens, OpenID Connect, ECDSA etc. 53 | * Easy to deploy and use 54 | * Easy to integrate with corp identity systems 55 | * Applications can authentice over non-secure network (HTTP) 56 | * Extend to fit your requirements. For example, you may write a reverse and forward proxy spartan plugins 57 | * Protection against replay attacks and scoped capabilities 58 | 59 | ## Getting Started 60 | Please refer to [Getting Started Guide][] 61 | 62 | ## Language Bindings 63 | Following are the Spartan API language bindings. 64 | APIs are available to 65 | 66 | 1. Get tokens from attestation service (typically needed on your client application) 67 | 2. Validate tokens received in the request(typically needed on your server which accepts requests) 68 | 69 | * [Go](https://github.com/yahoo/spartan-go) 70 | * [Node.JS](https://github.com/yahoo/spartan-node) 71 | 72 | [Getting Started Guide]: doc/getting-started.markdown 73 | 74 | ## Acknowledgements 75 | Yahoo Paranoids team 76 | -------------------------------------------------------------------------------- /server/routes/user.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | var express = require('express'); 7 | var router = express.Router(); 8 | var models = require('../models'); 9 | var router_utils = require('./router-utils'); 10 | var auth = require('./user-auth'); 11 | 12 | router.post('/create', function (req, res) { 13 | // TODO: add JWT middleware here 14 | var userid = req.body.userid, 15 | type = req.body.type, 16 | userkey = req.body.userkey, 17 | name = req.body.name, 18 | createdBy = req.body.createdBy, // TODO: get this from JWT or else where 19 | role = req.body.role; 20 | 21 | if (userid && userkey && createdBy) { //mandatory fields 22 | models.User.create({ 23 | userid: userid, 24 | userkey: auth.hashGen(userkey), 25 | name: name, 26 | createdBy: createdBy, 27 | role: role || 'ADMIN', // default is ADMIN 28 | type: type || 'E' 29 | }).then(function () { 30 | return router_utils.sendSuccessResponse(res, { 31 | 'msg': 'created ' + userid 32 | }, 201); 33 | }).catch(function (e) { 34 | console.log("unable to create record.."); 35 | console.log(e); 36 | return router_utils.sendErrorResponse(res, { 37 | 'msg': 'Unable to create user' 38 | }, 500); 39 | }); 40 | 41 | } else { 42 | router_utils.sendErrorResponse(res, { 43 | 'msg': 'Mandatory field(s) missing' 44 | }); 45 | } 46 | }); 47 | 48 | 49 | router.post('/update', function (req, res) { 50 | // TODO: add JWT middleware here 51 | var userid = req.body.userid, 52 | type = req.body.type, 53 | userkey = req.body.userkey, 54 | name = req.body.name, 55 | role = req.body.role; 56 | 57 | if (userid) { //mandatory fields 58 | models.User.find({ 59 | userid: userid, 60 | }).then(function (user) { 61 | if (user) { 62 | user.updateAttributes({ 63 | userkey: userkey || user.userkey, 64 | name: name || user.name, 65 | role: role || user.role, 66 | type: type || user.type 67 | }).then(function () { 68 | return router_utils.sendSuccessResponse(res, { 69 | 'msg': 'updated data for ' + userid 70 | }); 71 | }).catch(function (e) { 72 | return router_utils.sendErrorResponse(res, { 73 | 'msg': 'unable to update record' 74 | }, 500); 75 | }); 76 | } else { 77 | return router_utils.sendErrorResponse(res, { 78 | 'msg': 'Not found' 79 | }, 404); 80 | } 81 | }).catch(function (e) { 82 | console.log("unable to update record.."); 83 | console.log(e); 84 | return router_utils.sendErrorResponse(res, { 85 | 'msg': 'Unable to update user' 86 | }, 500); 87 | }); 88 | 89 | } else { 90 | router_utils.sendErrorResponse(res, { 91 | 'msg': 'Mandatory field(s) missing' 92 | }); 93 | } 94 | }); 95 | 96 | 97 | router.post('/delete', function (req, res) { 98 | // TODO: add JWT middleware here 99 | var userid = req.body.userid; 100 | 101 | if (userid) { 102 | models.User.destroy({ 103 | where: { 104 | userid: userid 105 | } 106 | }).then(function () { 107 | return router_utils.sendSuccessResponse(res, { 108 | 'msg': 'deleted ' + userid 109 | }); 110 | }).catch(function (e) { 111 | console.log("unable to delete record.."); 112 | console.log(e); 113 | return router_utils.sendErrorResponse(res, { 114 | 'msg': 'Unable to delete user' 115 | }, 500); 116 | }); 117 | } else { 118 | router_utils.sendErrorResponse(res, { 119 | 'msg': 'Mandatory field(s) missing' 120 | }); 121 | } 122 | 123 | }); 124 | 125 | 126 | router.delete('/:userid', function (req, res) { 127 | // TODO: add JWT middleware here 128 | var userid = req.params.userid; 129 | 130 | if (userid) { 131 | models.User.destroy({ 132 | where: { 133 | userid: userid 134 | } 135 | }).then(function () { 136 | return router_utils.sendSuccessResponse(res, { 137 | 'msg': 'deleted ' + userid 138 | }); 139 | }).catch(function (e) { 140 | console.log("unable to delete record.."); 141 | console.log(e); 142 | return router_utils.sendErrorResponse(res, { 143 | 'msg': 'Unable to delete user' 144 | }, 500); 145 | }); 146 | } else { 147 | router_utils.sendErrorResponse(res, { 148 | 'msg': 'Mandatory field(s) missing' 149 | }); 150 | } 151 | 152 | }); 153 | 154 | 155 | router.get('/all/', function (req, res) { 156 | models.User.findAll().then(function (users) { 157 | return router_utils.sendSuccessResponseWObj(res, users); 158 | }).catch(function (e) { 159 | console.log("unable to get user record.."); 160 | console.log(e); 161 | return router_utils.sendErrorResponse(res, { 162 | 'msg': 'Unable to get any user' 163 | }, 500); 164 | }); 165 | }); 166 | 167 | 168 | router.get('/:userid', function (req, res) { 169 | var userid = req.params.userid; 170 | if (userid) { 171 | models.User.findAll({ 172 | where: { 173 | userid: userid 174 | } 175 | }).then(function (users) { 176 | return router_utils.sendSuccessResponseWObj(res, users); 177 | }).catch(function (e) { 178 | console.log("unable to get record.."); 179 | console.log(e); 180 | return router_utils.sendErrorResponse(res, { 181 | 'msg': 'Unable to get user' 182 | }, 500); 183 | }); 184 | 185 | } else { 186 | router_utils.sendErrorResponse(res, { 187 | 'msg': 'Mandatory field(s) missing' 188 | }); 189 | } 190 | 191 | }); 192 | 193 | module.exports = router; 194 | -------------------------------------------------------------------------------- /doc/security.md: -------------------------------------------------------------------------------- 1 | # Security 2 | [note: This repo is WIP and may not have all secuirty features implemented] 3 | 4 | Spartan IS a security infrastructure. The system is designed to protect against various attacks such as Man-in-the-Middle (MITM), token tampering/spoofing attacks 5 | 6 | The threat landscape for a system like spartan is pretty large. We consciously opt not to solve all peripheral security problems, instead focused on security of tokens and token exchange protocols that are core to our system. For example, though the expectation is to store app private key securely, we don't control the way you store and distribute. Our APIs do not accept key file paths, instead it accepts the keys loaded in memory. In that way you can distribute and store the keys securely in a way specific to your environment. 7 | 8 | ### What is not in scope? 9 | 10 | * Users who use spartan for their applications - by creating mappings between apps and roles. A user compromise can affect apps and role that person’s usergroup owns 11 | 12 | * Spartan admins: Spartan admins are the super users with complete control over spartan system. If an admin account is compromised, then its game over. 13 | 14 | * Client or server applications: If the client is compromised, it may impersonate the client to server 15 | 16 | User and admin compromise is a generic problem. There are different ways to mitigate those risks, such as enabling second factor authentication, using short lived credentials, etc. 17 | 18 | The client or a server host compromise is also a generic problem. It often occurs when an application has security vulnerabilities. The impact of an exploit also varies. The mitigation is to follow best security practices and above all follow good security hygiene when developing and deploying applications, and managing hosts. 19 | 20 | ### What is in scope? 21 | The main feature of spartan is to protect a resource (e.g. a service endpoint) from unauthorized access. The threat model is to enumerate all attacks and build defenses against those attacks. 22 | 23 | *So how an attacker can gain access to a spartan protected service endpoint?* 24 | 25 | 1. MITM and replay attacks 26 | 2. Gain access to provisioner or attestation service or steal AS private key 27 | 3. Weakness in crypto used in spartan 28 | 4. Compromise a user who has admin access to apps and roles. In that way attacker can add his app identity to an app to gain access to resource. 29 | 5. Compromise client hosts - the attacker can impersonate the client and gain access to protected service 30 | 31 | Spartan does not have direct protection against (4) and (5). Instead to mitigate the risk, user/admins must follow security best practices to protect their credentials and the applications/host that they manage. 32 | 33 | **MITM/replay attack** 34 | This is one of the main focus of our threat model. Given that the adversary is able to capture the request and replay it, how can we protect against such attacks 35 | 36 | * The recommended solution is to use TLS for transport security. This will provide protection against active eavesdropping attacks. 37 | * If the network communication is not protected (e.g. HTTP), spartan provides protection by: 38 | * Auth tokens are signed with client’s private key and are not reused (use nonce and short expiry). The server will cache nonce untill its expiry 39 | * Sign the request body with client’s private key so that the adversary cannot modify the request in transit 40 | 41 | **AS private key** 42 | Protection of attestation service private key(s) is paramount to the security of the whole system. This is mostly a deployment problem. We will provide guidelines to protect AS private key soon. 43 | 44 | 45 | **Crypto weaknesses** 46 | Spartan is using modern crypto technologies such as JWT and ECDSA algorithms. The system is dependent of open source crypto libraries. If these libraries contain security vulnerabilities, then that could be one risk. 47 | 48 | The nonce are generated using crypto random number generator functions and tokens are basically json web tokens (JWT) 49 | 50 | ## Identity 51 | An application instance is represented using its public key fingerprint (SHA256), The identity of the application instance is based on its private key, that means the knowledge of a private key is considered as a proof of identity. It is possible to impersonate a client if an intruder steals the private key from the application host. 52 | 53 | **Mitigation** 54 | 55 | 1. Generate key pairs inside container or VM, and the private key should not leave the host. 56 | 2. Key should not be repurposed or shared. In the world of dynamic provisioning, keys never persist for long period. 57 | 3. Soft check based on connecting client IP to see if the same keys are used elsewhere. The client IP is also attached to the AS token. This is not a strong assertion, but it can flag some potential issues. 58 | 59 | ## Trust 60 | A self-signed token by itself is not trusted or has no meaning. The trust is derived when it mapped to a spartan app or a role in the provisioner server. The app or the role's trust is derived from the user (user group) who owns it. 61 | 62 | AS token issued by attestation service binds application identity with its public key (fingerprint). The identity of the application is established from its app or role membership. 63 | 64 | ### Authorization 65 | **Client -> Attestation service** 66 | 67 | The authorization is provided by AStoken issued by attestation service. AS upon receving request from clients, authenticates the request by verifying client's self-signed token received as part of the request. It then extracts the subject ('sub') field (contains SHA256 public key fp) in the token and try to match with all mapped roles. If found, AS issues a token that asserts that the application is a member of requested role. Client can use this token to access a service protected by that role. The AStoken is scoped to a role, hence this token cannot to use to access other services/resources 68 | 69 | **Client -> Server** 70 | 71 | The client application self signs the AStoken and pass it as part of the request to access a protected service. The server application recevies the request, validate AStoken with AS public key. It also validates the client signature using the public key embedded in the request token. The server then computes a SHA256 of the client's public key embedded in the token and compare it with the SHA256 fingerprint embedded in the AS token (JWT 'sub' field). The client signature is trusted only if the fingerprints are same. Server also make sure the application is part of the server role before granting access. To thwart replay attacks, tokens are not reused and the token nonce is stored in the server side untill the token's expiry 72 | 73 | AStokens can be directly passed (with out client's signature) if the communication channel is secured (HTTPS). This will avoid the need to use new token for every request. 74 | -------------------------------------------------------------------------------- /server/routes/app-group.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | var models = require('../models'); 7 | var express = require('express'); 8 | var router = express.Router(); 9 | var router_utils = require('./router-utils'); 10 | var auth = require('./user-auth'); 11 | 12 | router.post('/create', [auth.verify], function (req, res) { 13 | var name = req.body.name, 14 | description = req.body.description, 15 | createdBy = req.token.sub, 16 | updatedBy = createdBy, 17 | userGroup = req.body.usergroup; 18 | 19 | if (name && createdBy && updatedBy && userGroup) { //mandatory fields 20 | models.UserInGroup.findOne({ 21 | where: { 22 | userGroupName: userGroup, 23 | userid: createdBy 24 | } 25 | }) 26 | .then(function (row) { 27 | if (row) { 28 | models.App.create({ 29 | name: name, 30 | description: description, 31 | createdBy: createdBy, 32 | updatedBy: updatedBy, 33 | ownedByUserGroup: userGroup 34 | }).then(function () { 35 | return router_utils.sendSuccessResponse(res, { 36 | 'msg': 'created ' + name 37 | }, 201); 38 | }).catch(function (e) { 39 | console.log("unable to create record.."); 40 | console.log(e); 41 | return router_utils.sendErrorResponse(res, { 42 | 'msg': 'Unable to create app' 43 | }, 500); 44 | }); 45 | } else { 46 | return router_utils.sendErrorResponse(res, { 47 | 'msg': 'Not allowed to create app' 48 | }, 403); 49 | } 50 | }) 51 | .catch(function (e) { 52 | console.log("unable to create app.."); 53 | console.log(e); 54 | return router_utils.sendErrorResponse(res, { 55 | 'msg': 'Unable to create app' 56 | }, 500); 57 | }); 58 | } else { 59 | router_utils.sendErrorResponse(res, { 60 | 'msg': 'Mandatory field(s) missing' 61 | }); 62 | } 63 | }); 64 | 65 | 66 | router.post('/update', [auth.verify, auth.authzUserApp], function (req, res) { 67 | var name = req.body.app, 68 | description = req.body.description, 69 | userGroup = req.body.usergroup, 70 | requestor = req.token.sub; 71 | 72 | // TODO: also add check to validate that requestor belongs to new usergroup ? 73 | if (name && requestor) { //mandatory fields 74 | models.App.find({ 75 | name: name, 76 | }).then(function (appGroup) { 77 | if (appGroup) { 78 | appGroup.updateAttributes({ 79 | description: description || appGroup.description, 80 | ownedByUserGroup: userGroup || appGroup.ownedByUserGroup, 81 | updatedBy: requestor 82 | }).then(function () { 83 | return router_utils.sendSuccessResponse(res, { 84 | 'msg': 'updated data for ' + name 85 | }); 86 | }).catch(function (e) { 87 | return router_utils.sendErrorResponse(res, { 88 | 'msg': 'unable to update record' 89 | }, 500); 90 | }); 91 | } else { 92 | router_utils.sendErrorResponse(res, { 93 | 'msg': 'Not allowed to update app group' 94 | }, 403); 95 | } 96 | }).catch(function (e) { 97 | console.log("unable to update record.."); 98 | console.log(e); 99 | return router_utils.sendErrorResponse(res, { 100 | 'msg': 'Unable to update app group' 101 | }, 500); 102 | }); 103 | } else { 104 | router_utils.sendErrorResponse(res, { 105 | 'msg': 'Mandatory field(s) missing' 106 | }); 107 | } 108 | }); 109 | 110 | 111 | 112 | router.post('/delete', [auth.verify, auth.authzUserApp], function (req, res) { 113 | var name = req.body.app, 114 | requestor = req.token.sub; 115 | 116 | if (name) { 117 | models.App.destroy({ 118 | where: { 119 | name: name 120 | } 121 | }).then(function () { 122 | return router_utils.sendSuccessResponse(res, { 123 | 'msg': 'deleted ' + name 124 | }); 125 | }).catch(function (e) { 126 | console.log("unable to delete record.."); 127 | console.log(e); 128 | return router_utils.sendErrorResponse(res, { 129 | 'msg': 'Unable to delete app group' 130 | }, 500); 131 | }); 132 | } else { 133 | router_utils.sendErrorResponse(res, { 134 | 'msg': 'Mandatory field(s) missing' 135 | }); 136 | } 137 | }); 138 | 139 | 140 | router.delete('/:app', [auth.verify, auth.authzUserApp], function (req, res) { 141 | var name = req.params.app, 142 | requestor = req.token.sub; 143 | 144 | if (name) { 145 | models.App.destroy({ 146 | where: { 147 | name: name 148 | } 149 | }).then(function () { 150 | return router_utils.sendSuccessResponse(res, { 151 | 'msg': 'deleted ' + name 152 | }); 153 | }).catch(function (e) { 154 | console.log("unable to delete record.."); 155 | console.log(e); 156 | return router_utils.sendErrorResponse(res, { 157 | 'msg': 'Unable to delete app group' 158 | }, 500); 159 | }); 160 | } else { 161 | router_utils.sendErrorResponse(res, { 162 | 'msg': 'Mandatory field(s) missing' 163 | }); 164 | } 165 | }); 166 | 167 | 168 | router.get('/all/', [auth.verify], function (req, res) { 169 | models.App.findAll().then(function (appGroups) { 170 | return router_utils.sendSuccessResponseWObj(res, appGroups); 171 | }).catch(function (e) { 172 | console.log("unable to get app group records.."); 173 | console.log(e); 174 | return router_utils.sendErrorResponse(res, { 175 | 'msg': 'Unable to get any app group' 176 | }, 500); 177 | }); 178 | }); 179 | 180 | 181 | router.get('/:appgroup', [auth.verify], function (req, res) { 182 | var name = req.params.appgroup; 183 | if (name) { 184 | models.App.findAll({ 185 | where: { 186 | name: name 187 | } 188 | }).then(function (appGroup) { 189 | return router_utils.sendSuccessResponseWObj(res, appGroup); 190 | }).catch(function (e) { 191 | console.log("unable to get record.."); 192 | console.log(e); 193 | return router_utils.sendErrorResponse(res, { 194 | 'msg': 'Unable to get app group' 195 | }, 500); 196 | }); 197 | 198 | } else { 199 | router_utils.sendErrorResponse(res, { 200 | 'msg': 'Mandatory field(s) missing' 201 | }); 202 | } 203 | 204 | }); 205 | 206 | router.post('/addmember', [auth.verify, auth.authzUserApp], function (req, res) { 207 | var identity = req.body.identity, 208 | identityType = req.body.type, 209 | appName = req.body.app, 210 | requestor = req.token.sub, 211 | role = req.body.role; 212 | 213 | if (identity && appName && requestor) { 214 | models.MemberInApp.create({ 215 | identity: identity, 216 | appName: appName, 217 | identityType: identityType || 'SHA256-Fingerprint', 218 | role: role || 'DEFAULT', 219 | createdBy: requestor 220 | }).then(function () { 221 | return router_utils.sendSuccessResponse(res, { 222 | 'msg': 'added ' + identity + ' to ' + appName 223 | }); 224 | }).catch(function (e) { 225 | console.log("unable to create record.."); 226 | console.log(e); 227 | return router_utils.sendErrorResponse(res, { 228 | 'msg': 'Unable to add to app group' 229 | }, 500); 230 | }); 231 | } else { 232 | router_utils.sendErrorResponse(res, { 233 | 'msg': 'Mandatory field(s) missing' 234 | }); 235 | } 236 | }); 237 | 238 | 239 | router.post('/removemember', [auth.verify, auth.authzUserApp], function (req, 240 | res) { 241 | var identity = req.body.identity, 242 | identityType = req.body.type, 243 | requestor = req.token.sub; 244 | 245 | if (identity && requestor) { 246 | models.MemberInApp.destroy({ 247 | where: { 248 | identity: identity, 249 | identityType: identityType || 'SHA256-Fingerprint' 250 | } 251 | }).then(function () { 252 | return router_utils.sendSuccessResponse(res, { 253 | 'msg': 'removed ' + identity 254 | }); 255 | }).catch(function (e) { 256 | console.log("unable to delete record.."); 257 | console.log(e); 258 | return router_utils.sendErrorResponse(res, { 259 | 'msg': 'Unable to remove app member' 260 | }, 500); 261 | }); 262 | } else { 263 | router_utils.sendErrorResponse(res, { 264 | 'msg': 'Mandatory field(s) missing' 265 | }); 266 | } 267 | }); 268 | 269 | module.exports = router; 270 | -------------------------------------------------------------------------------- /server/routes/role-group.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | var models = require('../models'); 7 | var express = require('express'); 8 | var router = express.Router(); 9 | var router_utils = require('./router-utils'); 10 | var auth = require('./user-auth'); 11 | 12 | router.post('/create', [auth.verify], function (req, res) { 13 | var name = req.body.name, 14 | description = req.body.description, 15 | roleType = req.body.roletype, 16 | roleHandle = req.body.rolehandle, 17 | createdBy = req.token.sub, 18 | updatedBy = createdBy, 19 | userGroup = req.body.usergroup; 20 | 21 | // TODO: add check to validate that requestor belongs to usergroup 22 | if (name && createdBy && updatedBy && userGroup) { //mandatory fields 23 | models.UserInGroup.findOne({ 24 | where: { 25 | userGroupName: userGroup, 26 | userid: createdBy 27 | } 28 | }) 29 | .then(function (row) { 30 | if (row) { 31 | models.Role.create({ 32 | name: name, 33 | roleType: roleType || '', 34 | roleHandle: roleHandle || '', 35 | description: description, 36 | createdBy: createdBy, 37 | updatedBy: updatedBy, 38 | ownedByUserGroup: userGroup 39 | }).then(function () { 40 | return router_utils.sendSuccessResponse(res, { 41 | 'msg': 'created ' + name 42 | }, 201); 43 | }).catch(function (e) { 44 | console.log("unable to create record.."); 45 | console.log(e); 46 | return router_utils.sendErrorResponse(res, { 47 | 'msg': 'Unable to create role' 48 | }, 500); 49 | }); 50 | } else { 51 | return router_utils.sendErrorResponse(res, { 52 | 'msg': 'Not allowed to create role' 53 | }, 403); 54 | } 55 | }) 56 | .catch(function (e) { 57 | console.log("unable to create role.."); 58 | console.log(e); 59 | return router_utils.sendErrorResponse(res, { 60 | 'msg': 'Unable to create role' 61 | }, 500); 62 | }); 63 | } else { 64 | router_utils.sendErrorResponse(res, { 65 | 'msg': 'Mandatory field(s) missing' 66 | }); 67 | } 68 | }); 69 | 70 | 71 | router.post('/update', [auth.verify, auth.authzUserRole], function (req, res) { 72 | var name = req.body.role, 73 | description = req.body.description, 74 | userGroup = req.body.usergroup, 75 | roleType = req.body.roletype, 76 | roleHandle = req.body.rolehandle, 77 | updatedBy = req.token.sub; 78 | 79 | // TODO: add check to validate that requestor belongs to BOTH old and new usergroup 80 | if (name && updatedBy) { //mandatory fields 81 | models.Role.find({ 82 | name: name, 83 | }).then(function (role) { 84 | if (role) { 85 | role.updateAttributes({ 86 | description: description || role.description, 87 | roleType: roleType || role.roleType, 88 | roleHandle: roleHandle || role.roleHandle, 89 | ownedByUserGroup: userGroup || role.ownedByUserGroup, 90 | updatedBy: updatedBy 91 | }).then(function () { 92 | return router_utils.sendSuccessResponse(res, { 93 | 'msg': 'updated data for ' + name 94 | }); 95 | }).catch(function (e) { 96 | return router_utils.sendErrorResponse(res, { 97 | 'msg': 'unable to update record' 98 | }, 500); 99 | }); 100 | } else { 101 | return router_utils.sendErrorResponse(res, { 102 | 'msg': 'Not found' 103 | }, 404); 104 | } 105 | }).catch(function (e) { 106 | console.log("unable to update record.."); 107 | console.log(e); 108 | return router_utils.sendErrorResponse(res, { 109 | 'msg': 'Unable to update role' 110 | }, 500); 111 | }); 112 | 113 | } else { 114 | router_utils.sendErrorResponse(res, { 115 | 'msg': 'Mandatory field(s) missing' 116 | }); 117 | } 118 | }); 119 | 120 | 121 | 122 | router.post('/delete', [auth.verify, auth.authzUserRole], function (req, res) { 123 | var name = req.body.role; 124 | 125 | if (name) { 126 | models.Role.destroy({ 127 | where: { 128 | name: name 129 | } 130 | }).then(function () { 131 | return router_utils.sendSuccessResponse(res, { 132 | 'msg': 'deleted ' + name 133 | }); 134 | }).catch(function (e) { 135 | console.log("unable to delete record.."); 136 | console.log(e); 137 | return router_utils.sendErrorResponse(res, { 138 | 'msg': 'Unable to delete role' 139 | }, 500); 140 | }); 141 | } else { 142 | router_utils.sendErrorResponse(res, { 143 | 'msg': 'Mandatory field(s) missing' 144 | }); 145 | } 146 | 147 | }); 148 | 149 | 150 | router.delete('/:role', [auth.verify, auth.authzUserRole], function (req, res) { 151 | var name = req.params.role; 152 | 153 | if (name) { 154 | models.Role.destroy({ 155 | where: { 156 | name: name 157 | } 158 | }).then(function () { 159 | return router_utils.sendSuccessResponse(res, { 160 | 'msg': 'deleted ' + name 161 | }); 162 | }).catch(function (e) { 163 | console.log("unable to delete record.."); 164 | console.log(e); 165 | return router_utils.sendErrorResponse(res, { 166 | 'msg': 'Unable to delete role' 167 | }, 500); 168 | }); 169 | } else { 170 | router_utils.sendErrorResponse(res, { 171 | 'msg': 'Mandatory field(s) missing' 172 | }); 173 | } 174 | 175 | }); 176 | 177 | 178 | router.get('/all/', [auth.verify], function (req, res) { 179 | models.Role.findAll().then(function (roles) { 180 | return router_utils.sendSuccessResponseWObj(res, roles); 181 | }).catch(function (e) { 182 | console.log("unable to get role records.."); 183 | console.log(e); 184 | return router_utils.sendErrorResponse(res, { 185 | 'msg': 'Unable to get any role' 186 | }, 500); 187 | }); 188 | }); 189 | 190 | 191 | router.get('/:role', [auth.verify, auth.authzUserRole], function (req, res) { 192 | var name = req.params.role; 193 | if (name) { 194 | models.Role.findAll({ 195 | where: { 196 | name: name 197 | } 198 | }).then(function (role) { 199 | return router_utils.sendSuccessResponseWObj(res, role); 200 | }).catch(function (e) { 201 | console.log("unable to get record.."); 202 | console.log(e); 203 | return router_utils.sendErrorResponse(res, { 204 | 'msg': 'Unable to get role' 205 | }, 500); 206 | }); 207 | 208 | } else { 209 | router_utils.sendErrorResponse(res, { 210 | 'msg': 'Mandatory field(s) missing' 211 | }); 212 | } 213 | 214 | }); 215 | 216 | router.post('/addmember', [auth.verify, auth.authzUserRole], function (req, res) { 217 | var appName = req.body.appname, 218 | createdBy = req.token.sub, 219 | roleName = req.body.role, 220 | attribute = req.body.attribute; 221 | if (roleName && appName && createdBy) { 222 | models.AppInRole.create({ 223 | roleName: roleName, 224 | appName: appName, 225 | attribute: attribute || 'default', 226 | createdBy: createdBy 227 | }).then(function () { 228 | return router_utils.sendSuccessResponse(res, { 229 | 'msg': 'added ' + appName + ' to ' + roleName 230 | }); 231 | }).catch(function (e) { 232 | console.log("unable to create record.."); 233 | console.log(e); 234 | return router_utils.sendErrorResponse(res, { 235 | 'msg': 'Unable to add to role group' 236 | }, 500); 237 | }); 238 | } else { 239 | router_utils.sendErrorResponse(res, { 240 | 'msg': 'Mandatory field(s) missing' 241 | }); 242 | } 243 | }); 244 | 245 | router.post('/removemember', [auth.verify, auth.authzUserRole], function (req, 246 | res) { 247 | var appName = req.body.appname, 248 | roleName = req.body.role 249 | requestor = req.token.sub; 250 | if (roleName && appName && requestor) { 251 | models.AppInRole.destroy({ 252 | where: { 253 | roleName: roleName, 254 | appName: appName 255 | } 256 | }).then(function () { 257 | return router_utils.sendSuccessResponse(res, { 258 | 'msg': 'removed ' + roleName + ": " + appName + 259 | ' association' 260 | }); 261 | }).catch(function (e) { 262 | console.log("unable to delete record.."); 263 | console.log(e); 264 | return router_utils.sendErrorResponse(res, { 265 | 'msg': 'Unable to remove role member' 266 | }, 500); 267 | }); 268 | } else { 269 | router_utils.sendErrorResponse(res, { 270 | 'msg': 'Mandatory field(s) missing' 271 | }); 272 | } 273 | }); 274 | 275 | module.exports = router; 276 | -------------------------------------------------------------------------------- /doc/spartanX.md: -------------------------------------------------------------------------------- 1 | # SpartanX - X.509 Certificate-based Authorization Architecture 2 | (Originally conceived in Dec-2015) 3 | 4 | ## A Reference Architecture for: 5 | 1. Bootstrapping application/service identity (See detailed description here: [identity-bootstrapping.md](https://github.com/yahoo/spartan/blob/master/doc/identity-bootstrapping.md)) 6 | 2. Short-lived TLS X509 Certificates 7 | 2. X509 Certificate based service authorization 8 | 9 | ## Introduction 10 | 11 | Container technologies are revolutionizing the way we develop, build and deploy applications 12 | in large scale production environments. At Yahoo we use containers in our CI build farms and production 13 | environments, that are on-demand and dynamic in nature. Applications running in containers often need to 14 | connect to various internal/external services that require authentication and authorization. Authenticating 15 | client application to a server is a challenge in such dynamic environments because we cannot rely on traditional 16 | IP or hostname based checks. IP based authentication no longer works because (1) container IP is dynamic and often 17 | repurposed (2) containers often share IPs. Alternate options include the use of TLS client certificates and other 18 | key based authentication schemes. TLS client certificates provide authentication, but not authorization 19 | capabilities by its own. 20 | 21 | SpartanX is an extension to Spartan that issues short lived X.509 certificate and support role based authorization. 22 | This small but significant enhancement enable spartan to support both token (JWT) and X.509 certificate client 23 | authorization. The design philosophy is try not to over-engineering things, instead focus on simple 24 | (easy to understand, verify, integrate and use), but effective/scalable solutions. 25 | 26 | ## What is SpartanX ? 27 | When compared with existing spartan system, the architecture remains same (Spartan is a role based authz system), 28 | but spartanX adds an additional endpoint to spartan Attestation Service (AS) that issues X.509 certificates to applications. 29 | 30 | The new proposal to use a single X509 certificate for both authentication and authorization. It works without any 31 | custom extensions, libraries or configuration! 32 | 33 | In general X.509 certificates are meant for identities and are used for authentication. An X.509 certificate 34 | basically binds identity with a public key. In our model, we use X.509 certificate that binds role(s) with a 35 | public key. The role provides both authentication and authorization for client X.509 certificates 36 | 37 | 38 | 39 | ## High-level Flow 40 | * The flow is similar to existing spartan system. Steps (1) (2) and (3) are part of system provisioning/deployment (control plane). 41 | (4) (5) and (6) is the application runtime (data plane) 42 | * The user (or headless user) provisions the container (application), generate a public, private key pair and a self-signed certificate 43 | inside the application container. 44 | * The fingerprint of the public key is computed and is added to respective role in the provisioner service 45 | (think Provisioner Service as a mapping application that binds an application with a set of public key or its fingerprint) 46 | * In client side, use curl (or similar tools/libs) with self-signed client certificates and connect to attestation server (AS) server. 47 | AS then extracts public key from the client TLS certificate, compute the fingerprint and does a lookup for role membership. 48 | If found, AS server issues a short expiry (<24 hours) X.509 certificate with Subject DNAME set to appropriate **role**. 49 | For example, if App1 client wants to connect `x.s3.aws.com` instance, the Subject DNAME of the cert issued contains 50 | CNAME called: ```app1.roles.x.s3.aws.com.``` (other combinations/refinements on DNAMES, SANs are possible) 51 | 52 | Example curl call to Attestation Service: 53 | 54 | ```curl --cert ./app-cert.pem --key ./app-privkey.pem https://as.example.com/api/as/getx509certs?role=x.s3.aws.com``` 55 | 56 | In the server side, enable TLS client auth and set TLS client Subject CNAME to match ```app1.roles.x.s3.aws.com```. 57 | For example, In apache mode_ssl case, use SSL_CLIENT_S_DN_CN.Similar capabilities are available on all major servers 58 | The concept of roles can also extended to have more granular roles like ```admin.roles.x.s3.aws.com.``` 59 | and ```super-admin.roles.x.s3.aws.com.``` etc. 60 | 61 | The AS basically acts as root or an intermediate CA. That means the application server needs to trust AS public key. If the AS public 62 | key is cross-signed by a third party root CA (DigiCert, Verisign etc.) with proper name constraints, then even AS public key distribution 63 | is not required. If cross-signing is not an option, we can sign AS public key with an internal root CA 64 | and distribute root CA to all hosts. The advantage is that AS public key becomes an intermediate CA, and we 65 | keep the root CA offline - which is more secure and also enable periodic key rolling capabilities. 66 | 67 | ## Features/Advantages 68 | 69 | * The ability to issue short-lived TLS certificates. Private key by itself has no meaning, unless we bind it with a role 70 | * No key management hassles - No keys are stored elsewhere, copied or distributed. Keys are generated on the fly in 71 | the containers as part of application provisioning/deployment phase 72 | * The short lived certificates can also extend to server certificate as well; for instance Edge/Mini pods use case. if the AS root 73 | cert is cross-signed with an external trusted CA, the potential is huge 74 | * No token replay attack possible, because the auth is tied with TLS session 75 | * Better performance: Expensive crypto validation happens during the TLS handshake itself, hence no need to do it for every HTTP requests 76 | (part of same TLS session). In addition, we can also leverage TLS session reuse 77 | * Can even work with no custom software/library for both server and client side. The only requirement is standard TLS support. 78 | You can pull your certificate just by using a curl cmd-line tool 79 | * Scalable role based solution 80 | * Easily enable client auth for any application that supports TLS. Eg. MySQL access from Kubernetes cluster. Note that IP 81 | whitelist is not a reliable solution in dynamic environments 82 | * SpartanX can issue both X.509 certificate and JWT based app token, thereby covering different use-case scenarios 83 | 84 | ## Limitations 85 | 86 | * Since delegation through delegation is not possible one side effect with this is - in many cases, we need to pass the auth tokens 87 | to downstream servers. Without some kind of chaining support, it is hard to delegate/pass the token credentials. This is one 88 | reason we still need to support the current token based system - something easy to pass to other servers. 89 | 90 | * TLS pre-requisite. This solution works only with TLS, and in many cases, the communication is not over TLS (or not feasible 91 | to use TLS). 92 | 93 | ## Short-lived Certificates for Servers 94 | 95 | The above diagram was modeled for client auth. This concept is equally applicable for servers getting short lived service certificates. 96 | In that model, server makes calls to AS server and refresh the certificate based on role membership. 97 | 98 | ## Variations 99 | With the ability to deploy our application continuously and fast with CI/CD, instead of pulling/refreshing 100 | the certificates dynamically, it is much less hassle if we provision (step 2 in the diagram) certificates during the application deployment phase. 101 | Since the lifespan of these containers are short, no run-time refresh is necessary. We expect 102 | (and want to) to see more such run-time resource loading (eg. provisioning of secrets) move to deployment phase. This drastically reduces 103 | many run-time external dependencies - means app developers and SEs have fewer things to worry about. If we are following this mode, 104 | then instead of provisioning a self-signed cert (step 2), we drop a CA signed cert into application container, and these certificates are 105 | never reused. If the cert gets expired, then the app must be re-deployed with the new certificates. In general we should shift run-time 106 | dependencies to deploy-time dependencies if possible. 107 | 108 | ## User Identity 109 | 110 | SpartanX can issue X.509 certificate for users, that represent identity and the privilege. For user, we may either use Subject 111 | UID field that identifies the user or overload Subject CNAME with some special naming convention. User based certificates is an 112 | alternate solution to Kerberos in Grid and similar environments. 113 | 114 | ## Security Considerations 115 | 116 | The technologies (TLS, X.509 etc) we use are widely in use for many years. Our contribution is to make these technologies available 117 | to operate at scale to meet our security requirements. All standard security practices must be followed for deployment. 118 | 119 | Architecture/Design threat modeling (An exercise for security engineers, and we highly appreciate those efforts :-) ) 120 | 121 | ## References 122 | * http://www.ietf.org/rfc/rfc3820.txt 123 | * https://tools.ietf.org/html/rfc5755 124 | * http://italiangrid.github.io/voms/ 125 | -------------------------------------------------------------------------------- /server/spartan.sql: -------------------------------------------------------------------------------- 1 | -- MySQL dump 10.13 Distrib 5.6.26-74.0, for Linux (x86_64) 2 | -- 3 | -- Host: localhost Database: spartan3 4 | -- ------------------------------------------------------ 5 | -- Server version 5.6.26-74.0-log 6 | 7 | /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; 8 | /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; 9 | /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; 10 | /*!40101 SET NAMES utf8 */; 11 | /*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */; 12 | /*!40103 SET TIME_ZONE='+00:00' */; 13 | /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; 14 | /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; 15 | /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; 16 | /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; 17 | 18 | -- 19 | -- Table structure for table `AppInRoles` 20 | -- 21 | 22 | DROP TABLE IF EXISTS `AppInRoles`; 23 | /*!40101 SET @saved_cs_client = @@character_set_client */; 24 | /*!40101 SET character_set_client = utf8 */; 25 | CREATE TABLE `AppInRoles` ( 26 | `id` int(11) NOT NULL AUTO_INCREMENT, 27 | `attribute` varchar(32) DEFAULT NULL, 28 | `createdAt` datetime NOT NULL, 29 | `updatedAt` datetime NOT NULL, 30 | `createdBy` varchar(128) DEFAULT NULL, 31 | `appName` varchar(128) DEFAULT NULL, 32 | `roleName` varchar(128) DEFAULT NULL, 33 | PRIMARY KEY (`id`), 34 | KEY `createdBy` (`createdBy`), 35 | KEY `appName` (`appName`), 36 | KEY `roleName` (`roleName`), 37 | CONSTRAINT `AppInRoles_ibfk_1` FOREIGN KEY (`createdBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 38 | CONSTRAINT `AppInRoles_ibfk_2` FOREIGN KEY (`appName`) REFERENCES `Apps` (`name`) ON DELETE SET NULL ON UPDATE CASCADE, 39 | CONSTRAINT `AppInRoles_ibfk_3` FOREIGN KEY (`roleName`) REFERENCES `Roles` (`name`) ON DELETE SET NULL ON UPDATE CASCADE 40 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 41 | /*!40101 SET character_set_client = @saved_cs_client */; 42 | 43 | -- 44 | -- Table structure for table `Apps` 45 | -- 46 | 47 | DROP TABLE IF EXISTS `Apps`; 48 | /*!40101 SET @saved_cs_client = @@character_set_client */; 49 | /*!40101 SET character_set_client = utf8 */; 50 | CREATE TABLE `Apps` ( 51 | `id` int(11) NOT NULL AUTO_INCREMENT, 52 | `name` varchar(128) NOT NULL, 53 | `description` varchar(512) DEFAULT NULL, 54 | `createdAt` datetime NOT NULL, 55 | `updatedAt` datetime NOT NULL, 56 | `createdBy` varchar(128) DEFAULT NULL, 57 | `updatedBy` varchar(128) DEFAULT NULL, 58 | `ownedByUserGroup` varchar(128) DEFAULT NULL, 59 | PRIMARY KEY (`id`), 60 | UNIQUE KEY `name` (`name`), 61 | KEY `createdBy` (`createdBy`), 62 | KEY `updatedBy` (`updatedBy`), 63 | KEY `ownedByUserGroup` (`ownedByUserGroup`), 64 | CONSTRAINT `Apps_ibfk_1` FOREIGN KEY (`createdBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 65 | CONSTRAINT `Apps_ibfk_2` FOREIGN KEY (`updatedBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 66 | CONSTRAINT `Apps_ibfk_3` FOREIGN KEY (`ownedByUserGroup`) REFERENCES `UserGroups` (`name`) ON DELETE SET NULL ON UPDATE CASCADE 67 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 68 | /*!40101 SET character_set_client = @saved_cs_client */; 69 | 70 | -- 71 | -- Table structure for table `MemberInApps` 72 | -- 73 | 74 | DROP TABLE IF EXISTS `MemberInApps`; 75 | /*!40101 SET @saved_cs_client = @@character_set_client */; 76 | /*!40101 SET character_set_client = utf8 */; 77 | CREATE TABLE `MemberInApps` ( 78 | `id` int(11) NOT NULL AUTO_INCREMENT, 79 | `identity` varchar(1024) NOT NULL, 80 | `identityType` text, 81 | `role` varchar(32) DEFAULT NULL, 82 | `expiry` datetime DEFAULT NULL, 83 | `createdAt` datetime NOT NULL, 84 | `updatedAt` datetime NOT NULL, 85 | `appName` varchar(128) DEFAULT NULL, 86 | `createdBy` varchar(128) DEFAULT NULL, 87 | PRIMARY KEY (`id`), 88 | UNIQUE KEY `identity` (`identity`), 89 | KEY `appName` (`appName`), 90 | KEY `createdBy` (`createdBy`), 91 | CONSTRAINT `MemberInApps_ibfk_1` FOREIGN KEY (`appName`) REFERENCES `Apps` (`name`) ON DELETE SET NULL ON UPDATE CASCADE, 92 | CONSTRAINT `MemberInApps_ibfk_2` FOREIGN KEY (`createdBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE 93 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 94 | /*!40101 SET character_set_client = @saved_cs_client */; 95 | 96 | -- 97 | -- Table structure for table `Roles` 98 | -- 99 | 100 | DROP TABLE IF EXISTS `Roles`; 101 | /*!40101 SET @saved_cs_client = @@character_set_client */; 102 | /*!40101 SET character_set_client = utf8 */; 103 | CREATE TABLE `Roles` ( 104 | `id` int(11) NOT NULL AUTO_INCREMENT, 105 | `name` varchar(128) NOT NULL, 106 | `roleType` varchar(128) DEFAULT NULL, 107 | `roleHandle` varchar(128) DEFAULT NULL, 108 | `description` varchar(512) DEFAULT NULL, 109 | `createdAt` datetime NOT NULL, 110 | `updatedAt` datetime NOT NULL, 111 | `createdBy` varchar(128) DEFAULT NULL, 112 | `updatedBy` varchar(128) DEFAULT NULL, 113 | `ownedByUserGroup` varchar(128) DEFAULT NULL, 114 | `ownedByApp` varchar(128) DEFAULT NULL, 115 | PRIMARY KEY (`id`), 116 | UNIQUE KEY `name` (`name`), 117 | KEY `createdBy` (`createdBy`), 118 | KEY `updatedBy` (`updatedBy`), 119 | KEY `ownedByUserGroup` (`ownedByUserGroup`), 120 | KEY `ownedByApp` (`ownedByApp`), 121 | CONSTRAINT `Roles_ibfk_1` FOREIGN KEY (`createdBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 122 | CONSTRAINT `Roles_ibfk_2` FOREIGN KEY (`updatedBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 123 | CONSTRAINT `Roles_ibfk_3` FOREIGN KEY (`ownedByUserGroup`) REFERENCES `UserGroups` (`name`) ON DELETE SET NULL ON UPDATE CASCADE, 124 | CONSTRAINT `Roles_ibfk_4` FOREIGN KEY (`ownedByApp`) REFERENCES `Apps` (`name`) ON DELETE SET NULL ON UPDATE CASCADE 125 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 126 | /*!40101 SET character_set_client = @saved_cs_client */; 127 | 128 | -- 129 | -- Table structure for table `UserGroups` 130 | -- 131 | 132 | DROP TABLE IF EXISTS `UserGroups`; 133 | /*!40101 SET @saved_cs_client = @@character_set_client */; 134 | /*!40101 SET character_set_client = utf8 */; 135 | CREATE TABLE `UserGroups` ( 136 | `id` int(11) NOT NULL AUTO_INCREMENT, 137 | `name` varchar(128) NOT NULL, 138 | `description` varchar(512) DEFAULT NULL, 139 | `createdAt` datetime NOT NULL, 140 | `updatedAt` datetime NOT NULL, 141 | `createdBy` varchar(128) DEFAULT NULL, 142 | `updatedBy` varchar(128) DEFAULT NULL, 143 | PRIMARY KEY (`id`), 144 | UNIQUE KEY `name` (`name`), 145 | KEY `createdBy` (`createdBy`), 146 | KEY `updatedBy` (`updatedBy`), 147 | CONSTRAINT `UserGroups_ibfk_1` FOREIGN KEY (`createdBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 148 | CONSTRAINT `UserGroups_ibfk_2` FOREIGN KEY (`updatedBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE 149 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 150 | /*!40101 SET character_set_client = @saved_cs_client */; 151 | 152 | -- 153 | -- Table structure for table `UserInGroups` 154 | -- 155 | 156 | DROP TABLE IF EXISTS `UserInGroups`; 157 | /*!40101 SET @saved_cs_client = @@character_set_client */; 158 | /*!40101 SET character_set_client = utf8 */; 159 | CREATE TABLE `UserInGroups` ( 160 | `id` int(11) NOT NULL AUTO_INCREMENT, 161 | `userType` varchar(128) DEFAULT NULL, 162 | `role` varchar(32) DEFAULT NULL, 163 | `createdAt` datetime NOT NULL, 164 | `updatedAt` datetime NOT NULL, 165 | `userGroupName` varchar(128) DEFAULT NULL, 166 | `userid` varchar(128) DEFAULT NULL, 167 | `createdBy` varchar(128) DEFAULT NULL, 168 | PRIMARY KEY (`id`), 169 | KEY `userGroupName` (`userGroupName`), 170 | KEY `userid` (`userid`), 171 | KEY `createdBy` (`createdBy`), 172 | CONSTRAINT `UserInGroups_ibfk_1` FOREIGN KEY (`userGroupName`) REFERENCES `UserGroups` (`name`) ON DELETE SET NULL ON UPDATE CASCADE, 173 | CONSTRAINT `UserInGroups_ibfk_2` FOREIGN KEY (`userid`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE, 174 | CONSTRAINT `UserInGroups_ibfk_3` FOREIGN KEY (`createdBy`) REFERENCES `Users` (`userid`) ON DELETE SET NULL ON UPDATE CASCADE 175 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 176 | /*!40101 SET character_set_client = @saved_cs_client */; 177 | 178 | -- 179 | -- Table structure for table `Users` 180 | -- 181 | 182 | DROP TABLE IF EXISTS `Users`; 183 | /*!40101 SET @saved_cs_client = @@character_set_client */; 184 | /*!40101 SET character_set_client = utf8 */; 185 | CREATE TABLE `Users` ( 186 | `id` int(11) NOT NULL AUTO_INCREMENT, 187 | `userid` varchar(128) NOT NULL, 188 | `type` varchar(32) DEFAULT NULL, 189 | `userkey` varchar(4096) DEFAULT NULL, 190 | `name` varchar(128) DEFAULT NULL, 191 | `createdBy` varchar(128) DEFAULT NULL, 192 | `role` varchar(32) DEFAULT NULL, 193 | `domain` varchar(32) DEFAULT NULL, 194 | `createdAt` datetime NOT NULL, 195 | `updatedAt` datetime NOT NULL, 196 | PRIMARY KEY (`id`), 197 | UNIQUE KEY `userid` (`userid`) 198 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 199 | /*!40101 SET character_set_client = @saved_cs_client */; 200 | /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; 201 | 202 | /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; 203 | /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; 204 | /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; 205 | /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; 206 | /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; 207 | /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; 208 | /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; 209 | 210 | -- Dump completed on 2015-11-03 7:10:03 211 | -------------------------------------------------------------------------------- /server/routes/user-group.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright 2015, Yahoo Inc. 3 | // Copyrights licensed under the New BSD License. See the 4 | // accompanying LICENSE.txt file for terms. 5 | // 6 | var models = require('../models'); 7 | var express = require('express'); 8 | var router = express.Router(); 9 | var router_utils = require('./router-utils'); 10 | var auth = require('./user-auth'); 11 | 12 | router.post('/create', [auth.verify], function (req, res) { 13 | var name = req.body.name, 14 | description = req.body.description, 15 | createdBy = req.token.sub, 16 | updatedBy = createdBy, 17 | type = 'E', 18 | role = 'ADMIN'; 19 | 20 | if (name && createdBy && updatedBy) { //mandatory fields 21 | models.sequelize.transaction(function (t) { 22 | return models.UserGroup.create({ 23 | name: name, 24 | description: description, 25 | createdBy: createdBy, 26 | updatedBy: updatedBy 27 | }, { 28 | transaction: t 29 | }).then(function (user) { 30 | return models.UserInGroup.create({ 31 | userType: type, 32 | role: role, 33 | userGroupName: name, 34 | userid: createdBy, 35 | createdBy: createdBy 36 | }, { 37 | transaction: t 38 | }); 39 | }); 40 | }).then(function () { 41 | //committed 42 | console.log("transaction committed"); 43 | return router_utils.sendSuccessResponse(res, { 44 | 'msg': 'created ' + name 45 | }, 201); 46 | }).catch(function (err) { 47 | console.log("transaction had an error, rolling back.. "); 48 | console.log(err); 49 | return router_utils.sendErrorResponse(res, { 50 | 'msg': 'Unable to create user group' 51 | }, 500); 52 | }); 53 | 54 | } else { 55 | router_utils.sendErrorResponse(res, { 56 | 'msg': 'Mandatory field(s) missing' 57 | }); 58 | } 59 | }); 60 | 61 | 62 | router.post('/update', [auth.verify], function (req, res) { 63 | var name = req.body.name, 64 | description = req.body.description, 65 | updatedBy = req.token.sub; 66 | 67 | if (name && updatedBy) { //mandatory fields 68 | models.UserInGroup.findOne({ 69 | where: { 70 | role: 'ADMIN', 71 | userGroupName: name, 72 | userid: updatedBy 73 | } 74 | }) 75 | .then(function (row) { 76 | if (row) { 77 | models.UserGroup.find({ 78 | name: name, 79 | }).then(function (userGroup) { 80 | if (userGroup) { 81 | userGroup.updateAttributes({ 82 | description: description || userGroup.description, 83 | updatedBy: updatedBy 84 | }).then(function () { 85 | return router_utils.sendSuccessResponse(res, { 86 | 'msg': 'updated data for ' + name 87 | }); 88 | }).catch(function (e) { 89 | return router_utils.sendErrorResponse(res, { 90 | 'msg': 'unable to update record' 91 | }, 500); 92 | }); 93 | } else { 94 | return router_utils.sendErrorResponse(res, { 95 | 'msg': 'Not found' 96 | }, 404); 97 | } 98 | }); 99 | } else { 100 | return router_utils.sendErrorResponse(res, { 101 | 'msg': 'Not allowed to delete to user group' 102 | }, 403); 103 | } 104 | }).catch(function (e) { 105 | console.log("unable to update record.."); 106 | console.log(e); 107 | return router_utils.sendErrorResponse(res, { 108 | 'msg': 'Unable to update user group' 109 | }, 500); 110 | }); 111 | 112 | } else { 113 | router_utils.sendErrorResponse(res, { 114 | 'msg': 'Mandatory field(s) missing' 115 | }); 116 | } 117 | }); 118 | 119 | 120 | router.post('/delete', [auth.verify], function (req, res) { 121 | var name = req.body.name, 122 | requestor = req.token.sub; 123 | 124 | if (name) { 125 | models.UserInGroup.findOne({ 126 | where: { 127 | role: 'ADMIN', 128 | userGroupName: name, 129 | userid: requestor 130 | } 131 | }) 132 | .then(function (row) { 133 | if (row) { 134 | models.UserGroup.destroy({ 135 | where: { 136 | name: name 137 | } 138 | }).then(function () { 139 | return router_utils.sendSuccessResponse(res, { 140 | 'msg': 'deleted ' + name 141 | }); 142 | }).catch(function (e) { 143 | console.log("unable to delete record.."); 144 | console.log(e); 145 | return router_utils.sendErrorResponse(res, { 146 | 'msg': 'Unable to delete user group' 147 | }, 500); 148 | }); 149 | } else { 150 | return router_utils.sendErrorResponse(res, { 151 | 'msg': 'Not allowed to delete to user group' 152 | }, 403); 153 | } 154 | }).catch(function (e) { 155 | console.log("unable to delete record.."); 156 | console.log(e); 157 | return router_utils.sendErrorResponse(res, { 158 | 'msg': 'Unable to delete user group' 159 | }, 500); 160 | }); 161 | } else { 162 | router_utils.sendErrorResponse(res, { 163 | 'msg': 'Mandatory field(s) missing' 164 | }); 165 | } 166 | }); 167 | 168 | 169 | router.delete('/:usergroup', [auth.verify], function (req, res) { 170 | var name = req.params.usergroup, 171 | requestor = req.token.sub; 172 | 173 | // Also delete all corresponding entries in UserInGroup - ideally via sql cascade 174 | if (name) { 175 | models.UserInGroup.findOne({ 176 | where: { 177 | role: 'ADMIN', 178 | userGroupName: name, 179 | userid: requestor 180 | } 181 | }) 182 | .then(function (row) { 183 | if (row) { 184 | models.UserGroup.destroy({ 185 | where: { 186 | name: name 187 | } 188 | }).then(function () { 189 | return router_utils.sendSuccessResponse(res, { 190 | 'msg': 'deleted ' + name 191 | }); 192 | }).catch(function (e) { 193 | console.log("unable to delete record.."); 194 | console.log(e); 195 | return router_utils.sendErrorResponse(res, { 196 | 'msg': 'Unable to delete user group' 197 | }, 500); 198 | }); 199 | } else { 200 | return router_utils.sendErrorResponse(res, { 201 | 'msg': 'Not allowed to delete to user group' 202 | }, 403); 203 | } 204 | }); 205 | } else { 206 | router_utils.sendErrorResponse(res, { 207 | 'msg': 'Mandatory field(s) missing' 208 | }); 209 | } 210 | 211 | }); 212 | 213 | 214 | router.get('/all/', [auth.verify], function (req, res) { 215 | models.UserGroup.findAll().then(function (users) { 216 | return router_utils.sendSuccessResponseWObj(res, users); 217 | }).catch(function (e) { 218 | console.log("unable to get user group records.."); 219 | console.log(e); 220 | return router_utils.sendErrorResponse(res, { 221 | 'msg': 'Unable to get any user group' 222 | }, 500); 223 | }); 224 | }); 225 | 226 | 227 | router.get('/:usergroup', [auth.verify], function (req, res) { 228 | var name = req.params.usergroup; 229 | if (name) { 230 | models.UserGroup.findAll({ 231 | where: { 232 | name: name 233 | } 234 | }).then(function (userGroups) { 235 | return router_utils.sendSuccessResponseWObj(res, userGroups); 236 | }).catch(function (e) { 237 | console.log("unable to get record.."); 238 | console.log(e); 239 | return router_utils.sendErrorResponse(res, { 240 | 'msg': 'Unable to get user group' 241 | }, 500); 242 | }); 243 | } else { 244 | router_utils.sendErrorResponse(res, { 245 | 'msg': 'Mandatory field(s) missing' 246 | }); 247 | } 248 | }); 249 | 250 | router.post('/adduser', [auth.verify], function (req, res) { 251 | var userid = req.body.userid; 252 | var group = req.body.group; 253 | var createdBy = req.token.sub; 254 | var role = req.body.role; 255 | var type = req.body.usertype; 256 | if (group && userid && createdBy) { 257 | models.UserInGroup.findOne({ 258 | where: { 259 | role: 'ADMIN', 260 | userGroupName: group, 261 | userid: createdBy 262 | } 263 | }) 264 | .then(function (row) { 265 | if (row) { 266 | models.UserInGroup.findOrCreate({ 267 | where: { 268 | userid: userid, 269 | userGroupName: group 270 | }, 271 | defaults: { 272 | userType: type || 'E', 273 | role: role || 'MEMBER', // we only have special logic associated to 'ADMIN' 274 | createdBy: createdBy 275 | } 276 | }) 277 | .then(function () { 278 | return router_utils.sendSuccessResponse(res, { 279 | 'msg': 'added ' + userid + ' to ' + group 280 | }); 281 | }) 282 | .catch(function (e) { 283 | console.log("unable to create record.."); 284 | console.log(e); 285 | return router_utils.sendErrorResponse(res, { 286 | 'msg': 'Unable to add to user group' 287 | }, 500); 288 | }); 289 | } else { 290 | return router_utils.sendErrorResponse(res, { 291 | 'msg': 'Not allowed to add to user group' 292 | }, 403); 293 | } 294 | }) 295 | .catch(function (e) { 296 | console.log("unable to create record.."); 297 | console.log(e); 298 | return router_utils.sendErrorResponse(res, { 299 | 'msg': 'Unable to add to user group' 300 | }, 500); 301 | }); 302 | } else { 303 | router_utils.sendErrorResponse(res, { 304 | 'msg': 'Mandatory field(s) missing' 305 | }); 306 | } 307 | }); 308 | 309 | router.post('/removeuser', [auth.verify], function (req, res) { 310 | var userid = req.body.userid; 311 | var group = req.body.group; 312 | var requestor = req.token.sub; 313 | if (group && userid) { 314 | models.UserInGroup.findOne({ 315 | where: { 316 | role: 'ADMIN', 317 | userGroupName: group, 318 | userid: requestor 319 | } 320 | }) 321 | .then(function (row) { 322 | if (row) { 323 | models.UserInGroup.destroy({ 324 | where: { 325 | userGroupName: group, 326 | userid: userid 327 | } 328 | }).then(function () { 329 | return router_utils.sendSuccessResponse(res, { 330 | 'msg': 'removed ' + userid + ' from ' + group 331 | }); 332 | }).catch(function (e) { 333 | console.log("unable to delete record.."); 334 | console.log(e); 335 | return router_utils.sendErrorResponse(res, { 336 | 'msg': 'Unable to remove from user group' 337 | }, 500); 338 | }); 339 | } else { 340 | return router_utils.sendErrorResponse(res, { 341 | 'msg': 'Not allowed to remove from user group' 342 | }, 403); 343 | } 344 | }) 345 | .catch(function (e) { 346 | console.log("unable to create record.."); 347 | console.log(e); 348 | return router_utils.sendErrorResponse(res, { 349 | 'msg': 'Unable to add to user group' 350 | }, 500); 351 | }); 352 | } else { 353 | router_utils.sendErrorResponse(res, { 354 | 'msg': 'Mandatory field(s) missing' 355 | }); 356 | } 357 | }); 358 | 359 | 360 | module.exports = router; 361 | -------------------------------------------------------------------------------- /doc/identity-bootstrapping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2017-07-30 20:54ZCanvas 1Layer 1TenantOrchestratorSpartanXInstanceTenantOrchestratorSpartanXInstancet_token = GetAuthzToken(provider_id)LaunchApp(t_token)AuthorizeTenantAppAccess(t_token, p_token, app_id)Boot(app_id)Generate ECDSA key pair, CSRAddToTenantApp(p_token, app_id, public_key_fp, node_id)CheckAppMembership,Mint CertsGetCertificate(CSR, node_id)OnExitRevokeTenantAppAccess(app_id, p_token)X509 certs, id tokens etc.Launch ApplicationOnboardingProvision Tenant App (whitelist tenant's app)RescheduleRefreshloopKillInstance(p_token, app_id)p_token -> provider auth tokent_token -> tenant auth tokenRemoveFromTenantApp(p_token, public_key_fp)App lifecycleloopReturn public_key_fpnode_id -> Instance's node identityOnExitRegister Tenant App/NamespaceWorker NodeWorker Node 4 | --------------------------------------------------------------------------------