├── .gitignore ├── script ├── test └── bootstrap ├── .travis.yml ├── index.coffee ├── Gruntfile.js ├── package.json ├── README.md ├── src └── auth.coffee └── test └── auth-test.coffee /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /script/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # bootstrap environment 4 | source script/bootstrap 5 | 6 | mocha --compilers coffee:coffee-script -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "node" 4 | - "4.4" 5 | before_install: npm install -g grunt-cli 6 | cache: 7 | directories: 8 | - node_modules 9 | notifications: 10 | email: false 11 | -------------------------------------------------------------------------------- /script/bootstrap: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Make sure everything is development forever 4 | export NODE_ENV=development 5 | 6 | # Load environment specific environment variables 7 | if [ -f .env ]; then 8 | source .env 9 | fi 10 | 11 | if [ -f .env.${NODE_ENV} ]; then 12 | source .env.${NODE_ENV} 13 | fi 14 | 15 | npm install 16 | 17 | # Make sure coffee and mocha are on the path 18 | export PATH="node_modules/.bin:$PATH" -------------------------------------------------------------------------------- /index.coffee: -------------------------------------------------------------------------------- 1 | fs = require 'fs' 2 | path = require 'path' 3 | 4 | module.exports = (robot, scripts) -> 5 | scriptsPath = path.resolve(__dirname, 'src') 6 | fs.exists scriptsPath, (exists) -> 7 | if exists 8 | for script in fs.readdirSync(scriptsPath) 9 | if scripts? and '*' not in scripts 10 | robot.loadFile(scriptsPath, script) if script in scripts 11 | else 12 | robot.loadFile(scriptsPath, script) -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(grunt) { 4 | 5 | grunt.loadNpmTasks('grunt-mocha-test'); 6 | grunt.loadNpmTasks('grunt-release'); 7 | 8 | grunt.initConfig({ 9 | mochaTest: { 10 | test: { 11 | options: { 12 | reporter: 'spec', 13 | require: 'coffee-script' 14 | }, 15 | src: ['test/**/*.coffee'] 16 | } 17 | }, 18 | release: { 19 | options: { 20 | tagName: 'v<%= version %>', 21 | commitMessage: 'Prepared to release <%= version %>.' 22 | } 23 | }, 24 | watch: { 25 | files: ['Gruntfile.js', 'test/**/*.coffee'], 26 | tasks: ['test'] 27 | } 28 | }); 29 | 30 | grunt.event.on('watch', function(action, filepath, target) { 31 | grunt.log.writeln(target + ': ' + filepath + ' has ' + action); 32 | }); 33 | 34 | // load all grunt tasks 35 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 36 | 37 | grunt.registerTask('test', ['mochaTest']); 38 | grunt.registerTask('test:watch', ['watch']); 39 | grunt.registerTask('default', ['test']); 40 | }; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hubot-auth", 3 | "description": "Assign roles to users and restrict command access in other scripts", 4 | "version": "2.1.0", 5 | "author": "Alex Williams (http://github.com/aw)", 6 | "contributors": [ 7 | "technicalpickles", 8 | "tombell", 9 | "robertfwest", 10 | "superkarn", 11 | "omares", 12 | "rafaelfranca", 13 | "mizzy", 14 | "jhubert", 15 | "davereid", 16 | "patcon", 17 | "therealklanni", 18 | "ceci-woodward" 19 | ], 20 | "license": "MIT", 21 | "keywords": [ 22 | "hubot", 23 | "hubot-scripts", 24 | "authentication", 25 | "roles" 26 | ], 27 | "repository": { 28 | "type": "git", 29 | "url": "git://github.com/hubot-scripts/hubot-auth.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/hubot-scripts/hubot-auth/issues" 33 | }, 34 | "dependencies": { 35 | "coffee-script": "^1.10.0" 36 | }, 37 | "devDependencies": { 38 | "chai": "^3.5.0", 39 | "co": "^4.6.0", 40 | "grunt": "^0.4.5", 41 | "grunt-contrib-watch": "^1.0.0", 42 | "grunt-mocha-test": "^0.12.7", 43 | "grunt-release": "^0.13.0", 44 | "hubot": "^2.18.0", 45 | "hubot-test-helper": "^1.4.4", 46 | "matchdep": "^1.0.1", 47 | "mocha": "^2.4.5" 48 | }, 49 | "main": "index.coffee", 50 | "scripts": { 51 | "test": "grunt test" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hubot: hubot-auth 2 | 3 | [![Build Status](https://travis-ci.org/hubot-scripts/hubot-auth.svg?branch=master)](https://travis-ci.org/hubot-scripts/hubot-auth) 4 | 5 | Assign roles to users and restrict command access in other scripts. 6 | 7 | See [`src/auth.coffee`](src/auth.coffee) for full documentation. 8 | 9 | ## Installation 10 | 11 | Add **hubot-auth** to your `package.json` file: 12 | 13 | ``` 14 | npm install --save hubot-auth 15 | ``` 16 | 17 | Add **hubot-auth** to your `external-scripts.json`: 18 | 19 | ```json 20 | ["hubot-auth"] 21 | ``` 22 | 23 | Run `npm install` 24 | 25 | ## Sample Interaction 26 | 27 | ``` 28 | user1>> hubot user2 has jester role 29 | hubot>> OK, user2 has the jester role. 30 | ``` 31 | 32 | ## Sample Usage 33 | ### Restricting commands 34 | ```coffee 35 | module.exports = (robot) -> 36 | # Command listener 37 | robot.respond /some command/i, (msg) -> 38 | role = 'some-role' 39 | user = robot.brain.userForName(msg.message.user.name) 40 | return msg.reply "#{name} does not exist" unless user? 41 | unless robot.auth.hasRole(user, role) 42 | msg.reply "Access Denied. You need role #{role} to perform this action." 43 | return 44 | # Some commandy stuff 45 | msg.reply 'Command done!' 46 | ``` 47 | ### Example Interaction 48 | ``` 49 | user2>> hubot some command 50 | hubot>> Access Denied. You need role some-role to perform this action. 51 | user1>> hubot user2 has some-role role 52 | hubot>> OK, user2 has the some-role role. 53 | user2>> hubot some command 54 | hubot>> Command done! 55 | ``` 56 | -------------------------------------------------------------------------------- /src/auth.coffee: -------------------------------------------------------------------------------- 1 | # Description 2 | # Assign roles to users and restrict command access in other scripts. 3 | # 4 | # Configuration: 5 | # HUBOT_AUTH_ADMIN - A comma separate list of user IDs 6 | # 7 | # Commands: 8 | # hubot has role - Assigns a role to a user 9 | # hubot doesn't have role - Removes a role from a user 10 | # hubot what roles does have - Find out what roles a user has 11 | # hubot what roles do I have - Find out what roles you have 12 | # hubot who has role - Find out who has the given role 13 | # hubot list assigned roles - List all assigned roles 14 | # hubot what is my name - Tells you your name from persistent storage 15 | # hubot what is my id - tells you your id from persistent storage 16 | # 17 | # Notes: 18 | # * Call the method: robot.auth.hasRole(msg.envelope.user,'') 19 | # * returns bool true or false 20 | # 21 | # * the 'admin' role can only be assigned through the environment variable 22 | # * roles are all transformed to lower case 23 | # 24 | # * The script assumes that user IDs will be unique on the service end as to 25 | # correctly identify a user. Names were insecure as a user could impersonate 26 | # a user 27 | 28 | config = 29 | admin_list: process.env.HUBOT_AUTH_ADMIN 30 | 31 | module.exports = (robot) -> 32 | 33 | unless config.admin_list? 34 | robot.logger.warning 'The HUBOT_AUTH_ADMIN environment variable not set' 35 | 36 | if config.admin_list? 37 | admins = config.admin_list.split ',' 38 | else 39 | admins = [] 40 | 41 | class Auth 42 | isAdmin: (user) -> 43 | user.id.toString() in admins 44 | 45 | hasRole: (user, roles) -> 46 | userRoles = @userRoles(user) 47 | if userRoles? 48 | roles = [roles] if typeof roles is 'string' 49 | for role in roles 50 | return true if role in userRoles 51 | return false 52 | 53 | usersWithRole: (role) -> 54 | users = [] 55 | for own key, user of robot.brain.data.users 56 | if @hasRole(user, role) 57 | users.push(user.name) 58 | users 59 | 60 | userRoles: (user) -> 61 | roles = [] 62 | if user? and robot.auth.isAdmin user 63 | roles.push('admin') 64 | if user.roles? 65 | roles = roles.concat user.roles 66 | roles 67 | 68 | robot.auth = new Auth 69 | 70 | 71 | robot.respond /@?(.+) ha(?:s|ve) (["'\w: -_]+) role/i, (msg) -> 72 | name = msg.match[1].trim() 73 | if name.toLowerCase() is 'i' then name = msg.message.user.name 74 | if name.match(/(.*)(?:don['’]t|doesn['’]t|do not|does not)/i) then return 75 | 76 | unless name.toLowerCase() in ['', 'who', 'what', 'where', 'when', 'why'] 77 | unless robot.auth.isAdmin msg.message.user 78 | msg.reply "Sorry, only admins can assign roles." 79 | else 80 | newRole = msg.match[2].trim().toLowerCase() 81 | 82 | user = robot.brain.userForName(name) 83 | return msg.reply "#{name} does not exist" unless user? 84 | user.roles or= [] 85 | 86 | if newRole in user.roles 87 | msg.reply "#{name} already has the '#{newRole}' role." 88 | else 89 | if newRole is 'admin' 90 | msg.reply "Sorry, the 'admin' role can only be defined in the HUBOT_AUTH_ADMIN env variable." 91 | else 92 | myRoles = msg.message.user.roles or [] 93 | user.roles.push(newRole) 94 | msg.reply "OK, #{name} has the '#{newRole}' role." 95 | 96 | robot.respond /@?(.+) (?:don['’]t|doesn['’]t|do not|does not) have (["'\w: -_]+) role/i, (msg) -> 97 | name = msg.match[1].trim() 98 | if name.toLowerCase() is 'i' then name = msg.message.user.name 99 | 100 | unless name.toLowerCase() in ['', 'who', 'what', 'where', 'when', 'why'] 101 | unless robot.auth.isAdmin msg.message.user 102 | msg.reply "Sorry, only admins can remove roles." 103 | else 104 | newRole = msg.match[2].trim().toLowerCase() 105 | 106 | user = robot.brain.userForName(name) 107 | return msg.reply "#{name} does not exist" unless user? 108 | user.roles or= [] 109 | 110 | if newRole is 'admin' 111 | msg.reply "Sorry, the 'admin' role can only be removed from the HUBOT_AUTH_ADMIN env variable." 112 | else 113 | myRoles = msg.message.user.roles or [] 114 | user.roles = (role for role in user.roles when role isnt newRole) 115 | msg.reply "OK, #{name} doesn't have the '#{newRole}' role." 116 | 117 | robot.respond /what roles? do(es)? @?(.+) have\?*$/i, (msg) -> 118 | name = msg.match[2].trim() 119 | if name.toLowerCase() is 'i' then name = msg.message.user.name 120 | user = robot.brain.userForName(name) 121 | return msg.reply "#{name} does not exist" unless user? 122 | userRoles = robot.auth.userRoles(user) 123 | 124 | if userRoles.length == 0 125 | msg.reply "#{name} has no roles." 126 | else 127 | msg.reply "#{name} has the following roles: #{userRoles.join(', ')}." 128 | 129 | robot.respond /who has (["'\w: -_]+) role\?*$/i, (msg) -> 130 | role = msg.match[1] 131 | userNames = robot.auth.usersWithRole(role) if role? 132 | 133 | if userNames.length > 0 134 | msg.reply "The following people have the '#{role}' role: #{userNames.join(', ')}" 135 | else 136 | msg.reply "There are no people that have the '#{role}' role." 137 | 138 | robot.respond /list assigned roles/i, (msg) -> 139 | roles = [] 140 | unless robot.auth.isAdmin msg.message.user 141 | msg.reply "Sorry, only admins can list assigned roles." 142 | else 143 | for i, user of robot.brain.data.users when user.roles 144 | roles.push role for role in user.roles when role not in roles 145 | if roles.length > 0 146 | msg.reply "The following roles are available: #{roles.join(', ')}" 147 | else 148 | msg.reply "No roles to list." 149 | 150 | robot.respond /what(?:'s|s|\s+is)\s+my\s+name\s*(?:\?|)/i, (msg) -> 151 | user = robot.brain.userForId(msg.envelope.user['id']) 152 | unless user and user['name'] 153 | msg.reply "Your user could not be found in my Brain, sorry!" 154 | return 155 | msg.reply "Your name is: #{user['name']}." 156 | 157 | robot.respond /what(?:'s|s|\s+is)\s+my\s+id\s*(?:\?|)/i, (msg) -> 158 | user = robot.brain.userForId(msg.envelope.user['id']) 159 | unless user and user['id'] 160 | msg.reply "Your user could not be found in my Brain, sorry!" 161 | return 162 | msg.reply "Your ID is: #{user['id']}." 163 | -------------------------------------------------------------------------------- /test/auth-test.coffee: -------------------------------------------------------------------------------- 1 | Helper = require("hubot-test-helper") 2 | helper = new Helper("../src") 3 | 4 | expect = require("chai").expect 5 | 6 | describe "auth", -> 7 | 8 | beforeEach -> 9 | process.env.HUBOT_AUTH_ADMIN = "alice" 10 | @room = helper.createRoom() 11 | @room.robot.brain.userForId "alice", 12 | name: "alice" 13 | 14 | @room.robot.brain.userForId "jimmy", 15 | name: "jimmy" 16 | 17 | @room.robot.brain.userForId "amy", 18 | name: "amy" 19 | 20 | @room.robot.brain.userForId "jimmy jones", 21 | name: "jimmy jones" 22 | 23 | afterEach -> 24 | @room.destroy() 25 | 26 | context " has role", -> 27 | 28 | it "admin user successfully sets role", -> 29 | @room.user.say("alice", "hubot: jimmy has demo role").then => 30 | expect(@room.messages).to.eql [ 31 | ["alice", "hubot: jimmy has demo role"] 32 | ["hubot", "@alice OK, jimmy has the 'demo' role."] 33 | ] 34 | 35 | 36 | it "admin user successfully sets role in the first-person", -> 37 | @room.user.say("alice", "hubot: I have demo role").then => 38 | expect(@room.messages).to.eql [ 39 | ["alice", "hubot: I have demo role"] 40 | ["hubot", "@alice OK, alice has the 'demo' role."] 41 | ] 42 | 43 | it "admin user successfully sets role for user with space in name", -> 44 | @room.user.say("alice", "hubot: jimmy jones has demo role").then => 45 | expect(@room.messages).to.eql [ 46 | ["alice", "hubot: jimmy jones has demo role"] 47 | ["hubot", "@alice OK, jimmy jones has the 'demo' role."] 48 | ] 49 | 50 | 51 | it "fail to add admin role via command", -> 52 | @room.user.say("alice", "hubot: jimmy has admin role").then => 53 | expect(@room.messages).to.eql [ 54 | ["alice", "hubot: jimmy has admin role"] 55 | ["hubot", "@alice Sorry, the 'admin' role can only be defined in the HUBOT_AUTH_ADMIN env variable."] 56 | ] 57 | 58 | it "anon user fails to set role", -> 59 | @room.user.say("amy", "hubot: jimmy has demo role").then => 60 | expect(@room.messages).to.eql [ 61 | ["amy", "hubot: jimmy has demo role"] 62 | ["hubot", "@amy Sorry, only admins can assign roles."] 63 | ] 64 | 65 | context " doesn't have role", -> 66 | it "admin user successfully removes role in the first-person", -> 67 | @room.user.say("alice", "hubot: alice has demo role").then => 68 | @room.user.say("alice", "hubot: I don't have demo role").then => 69 | expect(@room.messages).to.eql [ 70 | ["alice", "hubot: alice has demo role"] 71 | ["hubot", "@alice OK, alice has the 'demo' role."] 72 | ["alice", "hubot: I don't have demo role"] 73 | ["hubot", "@alice OK, alice doesn't have the 'demo' role."] 74 | ] 75 | 76 | it "fail to remove admin role via command", -> 77 | @room.user.say("alice", "hubot: jimmy doesn't have admin role").then => 78 | expect(@room.messages).to.eql [ 79 | ["alice", "hubot: jimmy doesn't have admin role"] 80 | ["hubot", "@alice Sorry, the 'admin' role can only be removed from the HUBOT_AUTH_ADMIN env variable."] 81 | ] 82 | 83 | 84 | it "admin user successfully removes role from user with space", -> 85 | @room.user.say("alice", "hubot: jimmy jones has demo role").then => 86 | @room.user.say("alice", "hubot: jimmy jones doesn't have demo role").then => 87 | expect(@room.messages).to.eql [ 88 | ["alice", "hubot: jimmy jones has demo role"] 89 | ["hubot", "@alice OK, jimmy jones has the 'demo' role."] 90 | ["alice", "hubot: jimmy jones doesn't have demo role"] 91 | ["hubot", "@alice OK, jimmy jones doesn't have the 'demo' role."] 92 | ] 93 | 94 | context "what roles does have", -> 95 | beforeEach -> 96 | @room.user.say("alice", "hubot: alice has demo role") 97 | 98 | it "successfully list multiple roles of admin user", -> 99 | @room.user.say("amy", "hubot: what roles does alice have?").then => 100 | expect(@room.messages).to.eql [ 101 | ["alice", "hubot: alice has demo role"] 102 | ["hubot", "@alice OK, alice has the 'demo' role."] 103 | ["amy", "hubot: what roles does alice have?"] 104 | ["hubot", "@amy alice has the following roles: admin, demo."] 105 | ] 106 | 107 | context "who has role", -> 108 | it "list admin users", -> 109 | @room.user.say("alice", "hubot: who has admin role?").then => 110 | expect(@room.messages).to.eql [ 111 | ["alice", "hubot: who has admin role?"] 112 | ["hubot", "@alice The following people have the 'admin' role: alice"] 113 | ] 114 | 115 | it "list admin users using non-admin user", -> 116 | @room.user.say("amy", "hubot: who has admin role?").then => 117 | expect(@room.messages).to.eql [ 118 | ["amy", "hubot: who has admin role?"] 119 | ["hubot", "@amy The following people have the 'admin' role: alice"] 120 | ] 121 | 122 | context "list assigned roles", -> 123 | it "successfully list assigned roles", -> 124 | @room.user.say("alice", "hubot: alice has demo role").then => 125 | @room.user.say("alice", "hubot: amy has test role").then => 126 | @room.user.say("alice", "hubot: alice has test role").then => 127 | @room.user.say("alice", "hubot: list assigned roles").then => 128 | expect(@room.messages).to.eql [ 129 | ["alice", "hubot: alice has demo role"] 130 | ["hubot", "@alice OK, alice has the 'demo' role."] 131 | ["alice", "hubot: amy has test role"] 132 | ["hubot", "@alice OK, amy has the 'test' role."] 133 | ["alice", "hubot: alice has test role"] 134 | ["hubot", "@alice OK, alice has the 'test' role."] 135 | ["alice", "hubot: list assigned roles"] 136 | ["hubot", "@alice The following roles are available: demo, test"] 137 | ] 138 | 139 | it "successfully lists roles of user with space in name", -> 140 | @room.user.say("alice", "hubot: jimmy jones has demo role").then => 141 | @room.user.say("amy", "hubot: what roles does jimmy jones have?").then => 142 | expect(@room.messages).to.eql [ 143 | ["alice", "hubot: jimmy jones has demo role"] 144 | ["hubot", "@alice OK, jimmy jones has the 'demo' role."] 145 | ["amy", "hubot: what roles does jimmy jones have?"] 146 | ["hubot", "@amy jimmy jones has the following roles: demo."] 147 | ] 148 | 149 | context "what is my name", -> 150 | it "returns the name", -> 151 | @room.user.say("alice", "hubot: what is my name?").then => 152 | expect(@room.messages).to.eql [ 153 | ["alice", "hubot: what is my name?"], 154 | ["hubot", "@alice Your name is: alice."] 155 | ] 156 | 157 | context "what is my id", -> 158 | it "returns the id", -> 159 | @room.user.say("alice", "hubot: what is my id?").then => 160 | expect(@room.messages).to.eql [ 161 | ["alice", "hubot: what is my id?"], 162 | ["hubot", "@alice Your ID is: alice."] 163 | ] --------------------------------------------------------------------------------