LIFULL Advent Calendar 2017の8日目の記事です。
普段は海外向けの不動産ポータルサイトの開発に携わっていますが
本記事ではそれとは関係のない個人的な話をします。
はじめに
背景
サービスがスケールすると運用業務が増えますが、時間は有限です。
効率化の一手段として、定型的な仕事をチャットボットに任せることを考えました。
導入に際して
今やチャットボットはどこでも導入されている仕組みですが
チャットボット自体の作成や運用にもそれなりに手間がかかります。
- ソースコードの管理(情報の秘匿)
- 使用するユーザの権限の管理
- チャットボット実行サーバの運用・監視・権限の管理
など。最初から一気に考えるとやる気とスピードを損ねそうです。
そこで
- ソースコードは自身のPCの中だけに秘めて
- 自分のタスクだけを実行するために
- ローカルでボットを動かす
スモールスタートで始めよう と思い立ちました。
導入
今回は Python製のErrbotを使用します。
(自分しか触らないので、選択の自由度は非常に高いです)
Setupに従ってチャットボット「benriya」さんを作成し
Slackの設定をするといい感じにやりとりができるようになりました。
完全に個人的な用途で言えば
togglで管理しているタスクをサマライズして表示させたりしています。
togglのダッシュボードではかゆいところに手が届かなかったりするので
APIの返り値を加工して体裁を整えて出力するようにしました。
なんとなく便利になりましたが、ここで マッチポンプ的な 問題が発生します。
問題
他の人に絡まれる。
私しか使わないつもりで APIキーはばっちりハードコーディングしたので
他の人が勝手に私のタスクを読み取ったり、サーバを操作したりできます。
(今回は極端な実装をしていますが)真面目な話で言うと
そもそもボット自身が強い権限を持つ可能性もあるので、場合によっては危険です。
我が子が変な人に声をかけられてもついていかないようにする必要があります。
アクセス制限
(ここからが本題)
Errbot には、各コマンドを実行できるユーザを制限する機能があります。
参照: Administration#Restricting access
BOT_ADMINS
Errbotの設定ファイルであるconfig.py
において
BOT_ADMINS
変数にはチャットボットの管理者のユーザ名を登録します。
なお、Slackの場合はこちらのページに正式なアカウント名が表示されます。
https://xxx.slack.com/account/settings#username
admin_onlyなコマンドの設定
ビルトインのコマンドの中では!restart
や!shutdown
など
管理者しか実行することができません。
ソースコードでは下記のようにadmin用コマンドとしての設定がされています。
@botcmd(admin_only=True)
def restart(self, msg, args):
@arg_botcmd('--confirm', dest="confirmed", action="store_true",
help="confirm you want to shut down", admin_only=True)
@arg_botcmd('--kill', dest="kill", action="store_true",
help="kill the bot instantly, don't shut down gracefully", admin_only=True)
def shutdown(self, msg, confirmed, kill):
!shutdown
はコマンドのオプションがあるため複雑に見えますが
コマンド作成のための@botcmd
や@arg_botcmd
というデコレータにおいて
引数としてadmin_only=True
が指定されているのがわかります。
!task
も同様に設定してみると、adminの私以外は使用できなくなります。
@botcmd(admin_only=True)
def task(self, msg, args):
管理者への通知
アクセス制限から少し話がそれますが
BotPlugin
クラスに実装されたwarn_admins()
を使用すると
BOT_ADMINS
に記載したユーザ全員にメッセージを通知します。
危険なコマンドや怪しい挙動を検知した際の通知に活用できそうです。
ACCESS_CONTROLS
今回の件はadmin_only
のコマンドを増やしていけば解決しますが
後の運用のためにその他のアクセス制限について確認します。
細やかなACLが必要な場合は、config.py
において
ACCESS_CONTROLS
やACCESS_CONTROLS_DEFAULT
を設定します。
詳細はconfig.pyのサンプルファイルに記載されています。
これらの項目では、特定のユーザや部屋(チャンネル?)に対して
コマンドの実行を許可する/拒否する ための設定を行います。
例えば、下記のサンプルではデフォルトですべてのアクセスを許しつつ
status
、about
、uptime
などのコマンドには個別の設定がされています。
# Example:
ACCESS_CONTROLS_DEFAULT = {} # デフォルトですべてのアクセスを許可する
ACCESS_CONTROLS = {'status': {'allowrooms': ('someroom@conference.localhost',)},
'about': {'denyusers': ('*@evilhost',), 'allowrooms': ('room1@conference.localhost', 'room2@conference.localhost')},
'uptime': {'allowusers': BOT_ADMINS},
'help': {'allowmuc': False},
'ChatRoom:*': {'allowusers': BOT_ADMINS},
}
設定箇所の違い
名称 | 説明 |
---|---|
ACCESS_CONTROLS_DEFAULT |
すべてのコマンドにデフォルトで適用するルールを記載 |
ACCESS_CONTROLS |
コマンドごとの詳細なルールを記載 |
ソースを読むと、ACCESS_CONTROLS_DEFAULT
を取得後に
コマンドの制限がACCESS_CONTROLS
に存在すれば上書きするようになっています。
acl = self.bot_config.ACCESS_CONTROLS_DEFAULT.copy()
for pattern, acls in self.bot_config.ACCESS_CONTROLS.items():
if ':' not in pattern:
pattern = '*:{command}'.format(command=pattern)
if ciglob(cmd_str, (pattern,)):
acl.update(acls)
break
使用可能なフィルタ
設定するルールは下記のフィルタで定義されます。
名称 | 説明 |
---|---|
allowusers |
使用可能なユーザを定義(ホワイトリスト) |
denyusers |
使用不可能なユーザを定義(ブラックリスト) |
allowrooms |
使用可能な部屋を定義(ホワイトリスト) |
denyrooms |
使用不可能な部屋を定義(ブラックリスト) |
allowprivate |
ダイレクトメッセージでのアクセスを許可(Bool) |
allowmuc |
部屋の中でのコマンド実行を許可(Bool) |
Slackでは「部屋」は「チャンネル」にあたるため
{'allowrooms': ('#general',)}
のようなかたちで記載するようです。
上記のフィルタはソース中では下記のようなロジックで評価されています。
allow_xxx
はdeny_xxx
よりも先に評価されているため
両方指定がある場合はホワイトリスト方式の指定が優先されます。
if 'allowusers' in acl and not glob(usr, acl['allowusers']):
return self.access_denied(msg, "You're not allowed to access this command from this user", dry_run)
if 'denyusers' in acl and glob(usr, acl['denyusers']):
return self.access_denied(msg, "You're not allowed to access this command from this user", dry_run)
if msg.is_group:
if not isinstance(msg.frm, RoomOccupant):
raise Exception('msg.frm is not a RoomOccupant. Class of frm: %s' % msg.frm.__class__)
room = str(msg.frm.room)
if 'allowmuc' in acl and acl['allowmuc'] is False:
return self.access_denied(msg, "You're not allowed to access this command from a chatroom", dry_run)
if 'allowrooms' in acl and not glob(room, acl['allowrooms']):
return self.access_denied(msg, "You're not allowed to access this command from this room", dry_run)
if 'denyrooms' in acl and glob(room, acl['denyrooms']):
return self.access_denied(msg, "You're not allowed to access this command from this room", dry_run)
elif 'allowprivate' in acl and acl['allowprivate'] is False:
return self.access_denied(
msg,
"You're not allowed to access this command via private message to me",
dry_run
)
その他の設定
コマンドの存在自体も隠したいような場合は下記の設定でTrueを設定します。
名称 | 説明 |
---|---|
HIDE_RESTRICTED_COMMANDS |
使用が制限されているコマンドをhelpで隠す(Bool) |
HIDE_RESTRICTED_ACCESS |
使用が制限されているコマンドの呼出におけるエラーメッセージを出さない(Bool) |
Command filters
既存のACL機能でも物足りない場合は
コマンドフィルタで独自のアクセス制限を実装します。
errbotは各コマンドの実行直前にこの関数を呼びます。
(前述のBOT_ADMINS
やACCESS_CONTROLS
での制限もこれで書かれています。)
ドキュメントによると下記のような構造でコマンドフィルタを定義します。
@cmdfilter
def some_filter(self, msg, cmd, args, dry_run):
# 実行をブロックしたい場合はすべてNoneを返す
return None, None, None
# それ以外は次のフィルタ(コマンド)用の引数を返す
return msg, cmd, args
時刻によってアクセスを制限するような
ユーザベースでの制限以外のコントロールに使用できそうです。
終わりに
Errbotを安全に使うために、アクセスコントロールの話をしました。
ちなみに、チャットボットの私物化も
やってみると意外とメリットがあります。
- 好きな言語や環境で取り組めて自由に事故れる(勉強になる)
- 多少の不便は自分で吸収するため、開発のスピード感がある
- 自分にしか必要ない機能も迷わず組み込める
- ただ定形のスクリプトを書くよりもレールに乗れて、愛着も湧く
安定したら、共用できるところはメンバーにも公開しようと思います。