Movable Typeの権限管理とパーミッションチェックについてもう一度まとめておく。
公開日 : 2017-12-25 14:40:00
メリー・クリスマス。
福島県郡山市にて、打ち合わせの合間にこれを書いてます。寒い。 さて、今回はMTの権限管理について解説してみたいと思います。
この記事は、Movable Type Advent Calendar 2017 最終日の記事です。
※ 以前にも一度「ツボ」については、書いてるのですが、再度纏めておきます。
MTの権限を管理するデータベースのテーブルとその役割
- mt_permission
- mt_role
- mt_group(Advancedのみ※なので、この記事では触れないこととします)
- mt_association
この4つのテーブルがMTの権限を管理するために使われるものです。
それぞれの役割とデータの意味を解説します。
mt_permission
ユーザーがWebサイト、ブログ、もしくはシステムに対して何が出来るかを格納しているテーブルです。permission_permissionsレコードに’(シングルコーテーション)で囲まれた権限の文字列がカンマ区切りで格納されており、MT(やプラグイン)はこのレコードを参照することで権限の有無をチェックします。
mt_role
権限のグループを纏めるためのテーブルです。このレコードとWebサイト、ブログを関連付けることで「どこに、何が出来るか」を定義することになります。尚、「システム権限」については、このテーブルは使われません。
mt_association
mt_role、mt_author、mt_blogの3つのレコードを関連付けるための中間テーブルです。role_id、blog_id、author_idの3つの関連付けにより、権限が定義されます。もちろん、複数のロールをユーザー、Webサイト/ブログに割り当てることができますので、MTの権限、つまりコードから参照されるmt_permissionのpermission_permissionsカラムの値は、これらの定義から重複を取り除いて生成されたものとなります(つまり、 mt_permissionのpermission_permissionsカラムとは、正規化されておらず、例えていうならば、単なる権限のキャッシュと言えるでしょう。
※キャッシュをクリアして権限レコードを作り直すバッチ処理を書いていますので、宜しければ(cronなどに登録しておくと良いかと)。
mt_permissionのWebサイト/ブログ権限にはシステム権限がマージされる
MTの権限は、基本的には「ブラックリスト」形式にて、コード側で実装しなければなりません。コードにおける権限チェックは、下記のように行います。
- $app->can_do(permitted_action( 例: access_to_website_list) )
- $app->can_foo( 例: can_edit_templates )
該当ユーザーのWebサイト/ブログに対するmt_permissionレコードの取得は以下のようなコードで行われています。
$app->permissions(Webサイト/ブログのID)※システム権限の場合は0
これは、以下とコードと「ほぼ」同価です。
my $perm = MT->model('permission')->load( [ blog_id => Webサイト/ブログのID ] );
「ほぼ」と書いたのにはもちろん理由があるのですが、$app->permissions(Webサイト/ブログのID)で取得した際のmt_permissionのpermission_permissionsレコードは、実際のカラムの値とは違ったもとになっているのです。値には、システム権限がマージされた状態になっています。例えば’edit_templates’(テンプレートの編集)権限は、システムのテンプレートの編集権限があれば、各Webサイト/ブログに対してもテンプレート編集権限があることにされます。つまり、Webサイト/ブログに対するテンプレート編集権限を明示的に与えておらずとも、システム権限にて「テンプレートの編集」権限を与えていれば、Webサイト/ブログのmt_permissionのpermission_permissionsカラムには’edit_templates’という文字列が含まれることになることに注意してください。もしも、$perms(特定のWebサイト/ブログに対するmt_permissionレコード)に対して、$perms->saveというコードを走られたならば、Webサイト/ブログに対するmt_permissionレコードの値は汚染された状態にて置き換わってしまうことになります。つまり、これまでにも述べた通りこのカラムは所詮キャッシュに過ぎないわけですが、こうしてしまうとキャッシュが不正に改竄されてしまうことになります。
※ 正しくロードするのであれば、先程のコード
my $perm = MT->model('permission')->load( [ blog_id => Webサイト/ブログのID ] );
とするべきでしょう。
この仕様上、システム権限「ブログの作成(create_blog)」を持ったユーザーは、どのウェブサイトに対してもブログを作成できます。ウェブサイトに管理者を設定して、そのウェブサイトに「限り」ブログを作成する権限を与える、といったことはMTではできません(PowerCMSでは可能)。
MTの権限チェック=ブラックリスト形式であるということ
前半にも書きましたしこれまでに何度か発言してきましたが、MTの権限チェックは基本、ブラックリスト形式です。つまり、コードを書く、オブジェクトを作れば、そこには権限チェックはデフォルトでは入らず、実装者がチェックコードを個別に書かなくてはなりません。以前にも書いたと思いますが、例えばMT::Objectに独自のオブジェクトを登録してデータベースをアップグレードしたら!その瞬間では権限の有無にかかわらず、そのオブジェクトは「作成」「更新」「削除」できる状態になります。開発者はそれを「閉じる」コードを各自、書かなくてはなりません。
ドキュメントの問題
このドキュメントを見て、独自オブジェクトを作成したとします。(サンプルを使っても良いですが)データベースをアップグレードした段階で、このオブジェクトに対する権限チェック無く、作成・修正・削除ができる状態になっていることをご存知の開発者の方はどのくらいいるでしょうか。
POST /mt/mt.cgi?__mode=save&_type=foo&magic_token=[マジックトークンパラメタ]
save、deleteメソッドに_typeパラメタを渡すと、MT::CMS::Common::save(delete)に渡され、そこからェック無く、作成・修正・削除ができる状態になります。
MTの権限管理ドキュメント
権限とロールについては、以下のドキュメントに纏められていますが、これを見てコードが実装できるのだろうか。サンプルも無ければ説明も十分とは言えないかと思います。
ということでMTのCoreやプラグインで独自の権限を作成してチェックを行うまでのコードについて纏めます。
# MT::Core
'blog.administer_website' => {
'group' => 'blog_admin',
'inherit_from' => ['blog.administer_blog'],
'label' => 'Manage Website',
'order' => 200,
'permitted_action' => {
'save_all_settings_for_website' => 1,
'access_to_website_list' => 1,
'administer_website' => 1,
'clone_blog' => 1,
'delete_website' => 1,
'remove_user_assoc' => 1,
},
},
プラグインでconfig.yaml に書くなら、以下のようになります。
permissions:
blog.administer_website:
group: blog_admin
order: 200
label: Manage Website
inherit_from:
- blog.administer_blog
permitted_action:
save_all_settings_for_website: 1
access_to_website_list: 1
administer_website: 1
clone_blog: 1
delete_website: 1
remove_user_assoc: 1
group | ロール編集画面のどのブロックに表示されるか |
---|---|
inherit_from | 親権限。指定された権限を継承する(管理画面で自動的にチェックが付く) |
label | 権限名 |
order | 表示順 |
permitted_action | permitted_action $app->can_do('foo'); もしくは $app->permissions->can_do('foo') のように権限チェックに使われる※ |
※この、can_doについては、パラメタblog_idによって指定された $app->blogに対して行われることに注意
administer_website (Manage Website) 権限があるかどうか、だけをチェックするには、
$app->permissions->can_administer_website
通常は、superuser or の形になるため、$app->user->is_superuser || $app->permissions->can_administer_website
のようになります。
では、具体的に、この問題に対してどのようにすれば良いのかを説明します。
save、deleteメソッドに_typeパラメタを渡すと、MT::CMS::Common::save(delete)に渡され、そこからェック無く、作成・修正・削除ができる状態になります。
MT::CMS::Common::save(delete)に渡った際に、適切な権限チェックを入れるには3つの方法があります。
MT::App::CMS::core_disable_object_methods に fooの許可、不許可に関する指定を追記する
# MT::App::CMS
sub core_disable_object_methods {
my $app = shift;
return {
association => {
edit => 1,
save => 1,
},
banlist => { edit => 1, },
blocklist => {
save => 1,
delete => 1,
edit => 1,
},
...
残念ながら、プラグインでこれを指定する方法は(メソッドをオーバーライドすればできはするものの)、存在しません。Coreの開発をされる方は独自オブジェクトを登録したら、ここに追記するのを忘れると残念なことになります、
権限チェックコールバックを追加する
保存、削除、View(表示)に対応するコールバックは以下となります(括弧内は引数)。
- cms_save_permission_filter.foo ( $cb, $app, $id )
- cms_delete_permission_filter.foo( $cb, $app, $obj )
- cms_view_permission_filter.foo ( $cb, $app, $id, $obj(_promise) )
繰り返しになりますが、MTの権限チェックはブラックリスト形式です。オブジェクトを作成・登録したら、デフォルトで「穴」が空いているので、閉じないといけません。
※ 但し、superuserの場合はこのチェックは走らない(なぜだかcms_delete_permission_filterだけは走る)
save_foo, delete_foo, list_foo メソッドを登録する
これらのメソッドを追加登録すると、MT::CMS::Commonはそちらへ処理を投げるようになっています。
my $save_mode = 'save_' . $type;
if ( my $hdlrs = $app->handlers_for_mode($save_mode) ) {
return $app->forward($save_mode);
}
なので、該当するメソッドを追加して、その中で適切な権限チェックを行うことでオブジェクトに対する権限を指定するようにできます。
その上で更に注意すべき点
# いい加減疲れてきた。
このページで紹介している例であれば、以下の想定を行い、オブジェクトに対するチェックを行わないといけません。
- パラメタidに数字以外のものが渡された場合
- パラメタidにpageオブジェクトではなくentryオブジェクトのidが渡された場合
- パラメタidが空の場合
- パラメタidに現在のblogに属さないentry/pageのidが渡された場合
設計からやり直すとしたら、どうするとスマートだと思いますか?
とにかく、こんな感じでMTの権限チェックは中々に面倒です(MT7でそこが改善されるかどうかはわかりません)。MT7がどうなるにしても、まずは少なくとも一貫して理解可能なドキュメントの整備が急務ではないかと思います。
PowerCMS Xでは、どのようにしているかを書こうと思いましたが、次の打ち合わせが迫っているので、そこは次回としたいと思います。
現場からは以上です。それでは皆様、良いクリスマスとお正月をお迎え下さい。