Posted at

Hubot に権限管理機能を追加し, より安全に使用する

More than 5 years have passed since last update.


発端

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 を運用できる.