発端
Hubot に様々な機能を追加していると, 権限管理を行いたくなる.
- deploy は, A さんと B さんのみが行える
- Trello のチケット作成は, Chat Room に所属している者のみが行える
等々…
少し調べると, auth.coffee が見つかるが, Chat Room に制限が掛けられない.
Hubot は尻軽であるため, 招待されると, どのような Chat Room であっても顔を出してしまい, コマンドを入力されると, 素直に受け入れてしまう.
そこで, auth.coffee に Chat Room を判別する機能を追加しつつ, リファクタを試みる.
Source Code
auth.coffee
# Description:
# Auth allows you to assign roles to users which can be used by other scripts
# to restrict access to Hubot commands
#
# Dependencies:
# None
#
# Configuration:
# HUBOT_AUTH_ADMIN - A comma separate list of user IDs
# HUBOT_AUTH_ROOM - A comma separate list of room IDs
#
# Commands:
# hubot <user id> has <role> role - Assigns a role to a user
# hubot <user id> doesn't have <role> role - Removes a role from a user
# hubot what role does <user id> have - Find out what roles are assigned to a specific user
# hubot who has admin role - Find out who's an admin and can assign roles
#
# Notes:
# * Call the method: robot.auth.validUser(msg, '<role>')
# * returns bool true or false
#
# * the 'admin' role can only be assigned through the environment variable
# * roles are all transformed to lower case
#
# Author:
# IKUTA Masahito
module.exports = (robot) ->
admin = process.env.HUBOT_AUTH_ADMIN or ''
room = process.env.HUBOT_AUTH_ROOM or ''
class Auth
validUser: (msg, role) ->
user = msg.message.user
unless @inRoom(user)
msg.reply "Can not execute in this room."
return false
unless @hasRole(user, role)
msg.reply "#{user.name} has not the '#{role}' role."
return false
return true
hasRole: (user, role) ->
if @hasAdminRole(user.id)
return true
unless user.roles?
return false
return role in user.roles
hasAdminRole: (id) ->
return id.toString() in admin.toLowerCase().split(',')
inRoom: (user) ->
return user.room == room
robot.auth = new Auth
validParams = (msg, id, role) ->
if not validIDParam(msg, id)
return false
if not validRoleParam(msg, role)
return false
return true
validIDParam = (msg, id) ->
if isNaN(id)
msg.reply "ID is not integer."
return false
user = robot.brain.userForId(id)
unless user?
msg.reply "#{id} does not exist"
return false
if robot.auth.hasAdminRole(id)
msg.reply "ID have admin role."
return false
return true
validRoleParam = (msg, role) ->
if role == 'admin'
msg.reply "Sorry, the 'admin' role can only be defined in the HUBOT_AUTH_ADMIN env variable."
return false
return true
robot.respond /auth help$/i, (msg) ->
msg.reply '''
<user id> has <role> role - Assigns a role to a user
<user id> doesn't have <role> role - Removes a role from a user
what role does <user id> have - Find out what roles are assigned to a specific user
who has admin role - Find out who's an admin and can assign roles
'''
robot.respond /@?(.+) (has) (["'\w: -_]+) (role)/i, (msg) ->
unless robot.auth.validUser(msg, 'auth')
return
name = msg.match[1].trim()
newRole = msg.match[3].trim().toLowerCase()
if name == 'who' and newRole == 'admin'
msg.reply "The following people have the 'admin' role: #{admin.split(',')}"
return
id = parseInt(name)
if not validParams(msg, id, newRole)
return
user = robot.brain.userForId(id)
user.roles = user.roles or []
if newRole in user.roles
msg.reply "#{user.name} already has the '#{newRole}' role."
return
user.roles.push(newRole)
msg.reply "Ok, #{user.name} has the '#{newRole}' role."
robot.respond /@?(.+) (doesn't have|does not have) (["'\w: -_]+) (role)/i, (msg) ->
unless robot.auth.validUser(msg, 'auth')
return
id = parseInt(msg.match[1].trim())
newRole = msg.match[3].trim().toLowerCase()
if not validParams(msg, id, newRole)
return
user = robot.brain.userForId(id)
user.roles = (role for role in user.roles when role isnt newRole)
msg.reply "Ok, #{user.name} doesn't have the '#{newRole}' role."
robot.respond /(what role does|what roles does) @?(.+) (have)\?*$/i, (msg) ->
id = parseInt(msg.match[2].trim())
if not validIDParam(msg, id)
return
user = robot.brain.userForId(id)
user.roles = user.roles or []
if robot.auth.hasAdminRole(id)
isAdmin = ' and is also an admin'
else
isAdmin = ''
msg.reply "#{user.name} has the following roles: " + user.roles + isAdmin + "."
say.coffee
# Description:
# Hubot always has a snappy comeback.
#
# Dependencies:
# None
#
# Configuration:
# None
#
# Commands:
# hubot who am i - Display your name, id and room name.
#
# Notes:
# None
#
# Author:
# IKUTA Masahito
module.exports = (robot) ->
robot.respond /who am i$/i, (msg) ->
user = msg.message.user
msg.reply """
Name: #{user.name}
ID: #{user.id}
Room: #{user.room}
"""
使い方
say.coffee を設置し, 環境変数に設定する値を調べる.
Hubot> hubot who am i
Hubot>
Name: Shell
ID: 1
Room: Shell
値が得られたら, 環境変数を設定する.
bin/hubot-local
#!/bin/sh
npm install
export PATH="node_modules/.bin:node_modules/hubot/node_modules/.bin:$PATH"
export HUBOT_LOG_LEVEL="debug"
export HUBOT_AUTH_ADMIN="1"
export HUBOT_AUTH_ROOM="Shell"
exec node_modules/.bin/hubot "$@"
権限を設定する.
Hubot> hubot 2 has fabric role
Hubot> hubot 2 has trello role
Hubot> hubot what role does 2 have
Hubot> 2 has the following roles: trello,fabric.
各コマンドに権限を確認する処理を追加する.
trello.coffee
# ..snip..
module.exports = (robot) ->
robot.respond /trello (.*)$/i, (msg) ->
unless robot.auth.validUser(msg, 'trello')
return
# ..snip..
これで, 少し安全に hubot を運用できる.