Featured image of post 第8回:権限制御とエラーハンドリング【PerlでSlackボット指令センターを作る】

第8回:権限制御とエラーハンドリング【PerlでSlackボット指令センターを作る】

前回でアーキテクチャは完成しましたが、「動く」ことと「運用できる」ことは別物です。 本番環境で動かすボットには、予期せぬエラーや悪意ある操作に対する堅牢性が求められます。

今回は、Mediatorパターンだからこそ実現できる、一元化されたエラーハンドリングとセキュリティ対策について解説します。

セキュリティゲート

コマンドがクラッシュしたら?

今の実装では、もし execute メソッドの中で die が発生すると、ボット全体のプロセスが死んでしまったり、ユーザーに何も返事が返ってこなかったりします。これを防ぎましょう。

Mediatorの dispatch メソッドを修正し、eval (または Try::Tiny) でコマンド実行を包みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
use Try::Tiny;

sub dispatch {
    my ($self, $text, $user_role, $user_name) = @_;
    
    for my $cmd (@{$self->commands}) {
        if (my $args = $cmd->match($text)) {
            
            # ... 権限チェック ...

            my $result_msg;
            try {
                # 実行をトライ
                $result_msg = $cmd->execute($args);
                
                # 成功イベント
                $self->notify_observers({
                    type => 'success',
                    # ...
                });
            }
            catch {
                # エラー捕捉
                my $error = $_;
                $result_msg = "💥 コマンド実行中にエラーが発生しました: $error";
                
                # エラーイベント
                $self->notify_observers({
                    type    => 'error',
                    command => ref($cmd),
                    error   => $error,
                    user    => $user_name,
                });
            };
            
            return $result_msg;
        }
    }
    # ...
}

このように一箇所修正するだけで、すべてのコマンドに対してエラーハンドリングが適用されます。これがMediatorの強みです。

エラー通知用Observer

エラーが発生したときだけ、開発者チームのチャンネル(#dev-alerts)にメンションを飛ばしたいとします。 これも新しいObserverを作るだけで実現できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package Bot::Observer::ErrorHandler;
use Moo;
with 'Bot::Observer::Role';

sub update {
    my ($self, $event) = @_;
    
    # エラーイベント以外は無視
    return unless $event->{type} eq 'error';
    
    my $alert_msg = sprintf(
        "🚨 *Panic Alert*\nUser: %s\nCommand: %s\nError: ```%s```",
        $event->{user},
        $event->{command},
        $event->{error}
    );
    
    # 開発者チャンネルへ通知
    send_slack_alert($alert_msg); 
}

タイムアウト処理

デプロイコマンドなどが応答しなくなり、無限ループに入ったら? Perlの Sys::SigActionalarm を使って、コマンド実行に制限時間を設けることも、Mediatorなら一箇所で済みます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
use Sys::SigAction qw( set_sig_handler );

# ... dispatch内で ...

try {
    my $timeout = 30; # 秒
    eval {
        local $SIG{ALRM} = sub { die "Timeout\n" };
        alarm $timeout;
        $result_msg = $cmd->execute($args);
        alarm 0;
    };
    if ($@) { die $@ }; # エラー再送出
}
catch {
    if (/Timeout/) {
        $result_msg = "⏱️ 処理がタイムアウトしました (${timeout}秒)";
    }
    # ...
};

セキュリティ:Slack User IDマッピング

第5回でさらっと user_role を引数で渡していましたが、実際にはSlackのUser ID(例: U012ABCDEF)から権限を解決する必要があります。

これは、Webhookを受け取る層(Controller)で行うのが適切です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
my %USER_ROLES = (
    'U01234567' => 'admin',  # nobu
    'U99999999' => 'guest',  # other
);

# Webhookハンドラ内
my $slack_user_id = $payload->{event}->{user};
my $role = $USER_ROLES{$slack_user_id} // 'guest';

my $response = $mediator->dispatch($text, $role, $slack_user_id);

このように、「認証(誰か)」は入り口で、「認可(何ができるか)」はMediatorで行うという役割分担を意識しましょう。

次回はいよいよ最終回。これまでのすべてを統合し、実際に動く「指令センター」としての完成形をお見せします。 そして、この設計が私たちの開発チームに何をもたらしたのか、ChatOpsの真の価値について語ります。

今回の完成コード

エラーハンドリングとセキュリティ対策を追加した Bot::CommandMediator です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package Bot::CommandMediator;
use Moo;
use Try::Tiny;
use Sys::SigAction qw( set_sig_handler );
# ... プロパティ定義 ...

sub dispatch {
    my ($self, $text, $user_role, $user_name) = @_;
    
    for my $cmd (@{$self->commands}) {
        if (my $args = $cmd->match($text)) {
            
            if ($cmd->can('required_role') && $cmd->required_role ne $user_role) {
                return "⛔ 権限が不足しています(必要権限: " . $cmd->required_role . ")";
            }

            my $result_msg;
            try {
                my $timeout = 30;
                eval {
                    local $SIG{ALRM} = sub { die "Timeout\n" };
                    alarm $timeout;
                    $result_msg = $cmd->execute($args);
                    alarm 0;
                };
                if ($@) { die $@ };

                $self->notify_observers({
                    type         => 'success',
                    command_name => ref($cmd),
                    user         => $user_name,
                    message      => $result_msg,
                });
            }
            catch {
                my $error = $_;
                if ($error =~ /Timeout/) {
                    $result_msg = "⏱️ 処理がタイムアウトしました";
                    $error = "Timeout";
                } else {
                    $result_msg = "💥 エラーが発生しました: $error";
                }
                
                $self->notify_observers({
                    type    => 'error',
                    command => ref($cmd),
                    error   => $error,
                    user    => $user_name,
                });
            };
            
            return $result_msg;
        }
    }
    return "不明なコマンドです。";
}
1;
comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。