Categories
open all | close allTags
パソコン | Aptana | アクセス制御 | Subversion | 国際化 | CSRF | ドキュメント | タグ | モデル | テスト | 認証 | Migration | フォーム | rake | OpenID | Flash | デュアル・コア | スキンエンジン | 名称 | RESTfulSearch
«Prev || 1 || Next»
管理画面の抽象化と権限チェックの強化
あまり成果が上がっていない話ですが,引き続き管理画面の抽象化を考えています。どういうことかというと,表示する内容(メニューの文字やリンクなど)と,表示を制御する部分(CSS,HTMLのタグなど)を分け,さらにそれらを表示ロジックと分離させたいのです。ここまでできると,管理画面がほとんど着せ替えに近い格好で変えられ,さらに管理画面の機能強化も容易になります。
前述のようにテーブルについては既に完成し,かなりのロジック軽減につながっているのですが,メニュー表示などに広げるかどうかが悩みどころ。もちろん分離は可能なのですが,ロジック側や表示側の手間がかなりかかってしまい,どうもメリットがあるのかないのか微妙なところ。今は使わない方向で動いています。
もう一つ考え中なのが,管理画面での権限チェックを忘れない方法。
管理画面ではアクションごとに権限のチェックが必要ですが,権限のチェックをしなくても動いてしまうため,セキュリティ・ホールを作ってしまうおそれがあります。そこで権限のチェックをしていないときに動かなくするか,メッセージを表示するような仕組みを入れておきたいと思っています。今,これを書きながら一つアイディアが浮かんだので試してみます。
結果:AdminControllerにinitializeメソッドを作り,そこで@right_checkedというインスタンス変数をfalseで初期化します。権限チェックのメソッドで,これをtrueに変えます。ビューを実行するときにこの変数を見て,falseだとエラーメッセージを出すようにしました。
表出力の抽象化
管理画面の基本は表です。ブログやアイテムの一覧はもちろん,フォームもラベル部分と入力部分で表を使って表示することが多いと思います。そこで,管理画面の出力内容と表示を分離するため,表出力を抽象化してみました。
まず,出力内容はコントローラで次のような形で設定します。
def index
@blogs = @member.blogs
@table.header=[la('Blog Name'),la('Add Item'),la('Edit Item'),la('Comments'),\
la('Tags/Categories'),la('Settings'),('Delete Blog')]
@table.widths=[177]
@table.data = []
@blogs.each do |blog|
row = []
row << h(blog.bname)
row << context_link(new_admin_blog_item_url(@memberid, blog),blog,"commit", "page_add","add item")
row << context_link(admin_blog_items_url(@memberid, blog), blog, "commit", "page_edit","edit items")
row << context_link(admin_blog_comments_url(@memberid, blog),blog,"commentedit", "comments","comments")
row << context_link(admin_blog_categories_url(@memberid, blog),blog,"commentedit", "tag_red","categories")
row << context_link(edit_admin_blog_url(@memberid, blog),blog,"editsettings", "application_edit","settings")
row << context_link(admin_blog_url(@memberid, blog), blog,"deleteblog", "folder_delete","delete blog", \
:confirm=>'OK?', :method=>'delete')
@table.data << row
end
render
endここで@tableが表を抽象化したTable型のオブジェクトで,@table.headerに表頭,@table.widthsに列幅指定(あれば),@table.dataに表のボディ部分が入ります。
一方,表示側は
def show_table table
temp = <<HEAD
<div class="table">
<img src="/admin/img/bg-th-left.gif" width="8" height="7" alt="" class="left" />
<img src="/admin/img/bg-th-right.gif" width="7" height="7" alt="" class="right" />
HEAD
table.controller self
temp += table.html do |h|
h.options({:class=>'listing', :cellpadding=>"0", :cellspacing=>"0"})
h.head({:first=>'first', :last=>'last'})
h.body({:first=>'first style1', :last=>'last', :cycle=>[nil, 'bg']})
end
temp += '</div>'
endここでパラメータで与えられたtableが先ほどのTable型のオブジェクト。これに対して,table.htmlというメソッドで表の表示内容を指定しています。管理画面のすべての表に対して,このshow_table一つで対応できます。後はコンテンツ側を変えるだけ。
これで表部分はいいのですが,問題は表に入らないような要素がある場合。例えば,管理画面のトップページである,今の画面には新規ブログを作るボタンがあります。そこの部分は抽象化していないので,ビューに素のHTMLを書くことになってしまいました。今後はこういった要素の抽象化をするのか,あるいはこういった要素を排除していくのか考えていったほうがよさそうです。
力づくで解決
あまり,ここでばかり時間を費やしたくないので,コメントフォームを作ったときと同様,CSRF対策用のタグを手動で埋め込むことでとりあえず先に進むことにしました。ただ,さすがにフォームを作ったり,保存したりといったところは楽です。Railsのリソース・ルーティングにしたがってやっておくだけでほとんどはRailsが面倒見てくれる感じ。慣れたら,管理画面作るのはすいすいできそうです。というわけで,すごくいい加減(例えばデフォルトカテゴリはカテゴリIDを直接入れないといけない)ですが,ブログ設定をFoodynの管理画面で編集できるようになりました。
余談ですが「企業で使えるオープンソースCMS一挙12種類解説」という記事,なかなか参考になります。ステージ管理とか時限発行とか,考えていなかったなあと。時限発行は未来の日付で投稿された記事に対してpingなどを送る機能と同じような形で実現できそうな気がします。有効期限が切れた記事をドラフト扱いにするのか,遠い未来の投稿扱いにするのか,別のフラグを立てるのかといったところがちょっと悩ましいですが。
ステージ管理はどうやったらできるだろう。
初めての管理画面(笑)
とりあえず前言撤回で,RESTfulで用意されているもので大体なんとかできそうな感じがしてきました。deleteのユーザー・インタフェースなど,気になるところもありますが,まずは標準機能で実装していきたいと思います。で,一応今の管理画面。(管理画面)
URLは全部ではないですが,ちゃんとしたものを生成しています。
ここではメモとして,map.namespace付きのprefix付き,ネスティッド・リソースという“テンコ盛り”状態でのルーティングについて書いておきます。
map.namespace(:admin) do |admin|
admin.root :controller =>'admin'
admin.connect ':memberid', :controller =>'Admin'
admin.resources :blogs, :path_prefix=>'admin/:memberid' do |blog|
blog.resources :items
blog.resources :comments
blog.resources :categories
end
admin.resources :plugins, :path_prefix=>'memberid'
endというのがルーティング部分(まだ完全ではありません。必要なとこしか作ってないので)。ここでルートのコントローラを指定することで,アドミン用のコントローラはadminディレクトリの下から選ばれるようになります。アドミンへのアクセスは全部認証付きであり,認証されたユーザー名がmemberidになります。したがって'admin/:memberid'が管理画面のルートです。
その下のresourcesの部分が今日の骨子。ブログ周りのリソース定義をしています。RESTfulなルーティングを使うためにはresourcesが必要です。ここでちょっと注意が要るのがpath_prefixのところ,ここは本来':memberid'だけでいいはずなのですが,それだとURLを生成したときにadminが抜けてしまうバグ?があります。そこでわざわざこれを入れています。
以下でネストされたリソースを加えています。
URLを生成するときはリソースを指定する必要があるのですが,この場合,例えばアイテムの一覧を表示したかったら「admin_blog_items_url(@memberid, blog)」といったリソースをurl_forやlink_toなどのメソッドに渡します。ここで@memberidがユーザー名ですが,これをパラメータに入れるのも一つの肝。プレフィクスを指定しているからなのでしょうけれども,そのあたりの説明はドキュメントになく,試行錯誤で見つけました。なお,admin_blog_items_urlというメソッド名は「rake routes」でルート定義の一覧を出すと分かります。例えば今の部分は
admin_blog_items GET /admin/:memberid/blogs/:blog_id/items {:controller=>"admin/items", :action=>"index"}
といった形で記述されています。これで,今度はAdmin::ItemsControllerのindexメソッドを用意すればいいことが分かります。
これで後はスピードアップするかな?
権限を調べる部分の実装
まず,Memberのオブジェクトからはhas_rightというメソッドで権限を持っているかどうかを調べます。
# context is either a Blog, or an Item
def has_right rightname, context
#search role first.
context.has_right rightname, self
endrightnameが権限の名前,例えば"browse"などになります。contextはコメントにもあるように,どのコンテキストで権限を判断するかと言うこと。ブログあるいはアイテムがcontextです。で,これは実際にはコンテキストに問い合わせて調べます。つまり,BlogクラスとItemクラスにはどちらもhas_rightというメソッドがあってそれが呼び出されます。これが昨日書いたダック・タイピング,すなわち同じメソッドを持っていたら同じものとして使える,ということになります。
Blogクラスのこの部分の実装はこう。
def role_of member
if !member
return self.guest_role
else
t = Team.find(:first, :conditions=>["tblog=? and tmember=?", self.id, member.id])
if t then
return t.role
else
return self.user_role
end
end
end
def has_right rightname, member
role = self.role_of member
return role.attributes[rightname]
end
下のhas_rightがメインで,ロールに対してフィールド名で取得するようにしています。実はこれだと関係ないフィールドまで取れてしまいますが,そこはガチガチにしなくてもいいだろうと判断しています。上のrole_ofでは,ログインしていない場合(memberがnil),ログインしているけどチームに入っていない場合,チームに入っている場合に分けてRoleを返します。前二つの場合はBlogのテーブル内のフィールド情報から取り出します。guest_roleやuser_roleはクラスの上の方でbelongs_toとして定義しています。
一方Itemクラスは
def role_of member
role = self.blog.role_of member
itemrole = ItemRole.find(:first, :conditions=>["item_id=? and role_id=?",self.id, role.id])
if itemrole then
return itemrole
else
return role
end
end
def has_right rightname, member
role = self.role_of member
return role.attributes[rightname]
end
has_rightの部分は実は同じですが,ここでもダック・タイピングもどきのことをしています。アイテムで独自の権限を決める場合はその情報はItemRoleというクラスに入っていますが,その場合もRoleクラスと同じようにattributes[rightname]で結果を取り出しています。role_ofの方は先ほどのBlogのrole_ofでRoleを取り出し,ItemRoleが存在すればそっち,しなければ取ってきたRoleを返します。
管理画面を作り始める
まずはルーティングと認証から始めます。ルーティングはRails2の新機能であるmap.namespaceを使って表示系とは違うコントローラにします。具体的にはこんな感じ。
map.namespace(:admin) do |admin|
admin.root :controller =>'Admin'
admin.connect ':memberid', :controller =>'Admin'
endこれでadminディレクトリのadmin_controller.rbからAdmin::AdminControllerクラスをロードしようとします。
そちら側はこんな感じ
class Admin::AdminController < ActionController::Base
helper :all # include all helpers, all the time
before_filter :setmember
htpasswd :class=>"Member", :user=>"mname", :pass=>"mpassword" , :nofilter=>true
# See ActionController::RequestForgeryProtection for details
# Uncomment the :secret if you're not using the cookie session store
protect_from_forgery # :secret => '501bb263f43979b7993430648b7066d0'
def setmember
result = htpasswd_authorize
if @htpasswd_authorized_username
if !params['memberid']
redirect_to(:memberid=>@htpasswd_authorized_username)
else
if @htpasswd_authorized_username != params['memberid']
redirect_to(:memberid=>nil)
end
end
@member = Member.find_by_mname(@htpasswd_authorized_username)
end
return result
end
def index
render
end
end これで認証を通ってindexを実行するところまで動きます。
PS.TinyMCEを使ってみたのですが,こういったコードを入れるものとかとは相性よくなさそうです。Rucleus(仮)で採用しようかと考えていたのですが,別のエディタを使う方がよさそう。
«Prev || 1 || Next»