What have you found for these years?

2008-09-22

modeling photos/albums (2)

承上篇,加了不少新東西,不過還不是很完備,還需要再調整。

其中 photos_for_visitor 已經拉到 photos/albums 層級,
也就是說 Photo 會有 albums_for_visitor, Album 會有 photos_for_visitor.
兩者都是很單純地改寫成:resources.for_visitor(visitor, user)

但單單這樣取資料是不夠的,因為顯示頁面也需要存取限制。
像是如果閱讀一個 private album, 然後進去啥鬼都看不到,
實在沒什麼意義,這一頁本身就應該吐出 403 Forbidden.

我理想上是希望能做到類似這樣:


if @visitor.grant(album)
@photos = album.photos
else
render_optional_error_file 403
end

不過真的要做到這樣實在不容易,現在沒那麼多時間讓我慢慢試。
實際上目前我只做到:

@visitor.grant(resource, mode, callbacks.reverse_merge(:failed => lambda{
if @visitor.id == User::GuestID
CAS::Filter.filter self
else
render_optional_error_file 403
end
}))

也就是說,透過一個 block 做 abstraction, 而不在內部維護一個 granted 的 state.
上面針對 failed 的動作是檢查這個沒權限的人登入沒?有登入的話,就是 403.
沒登入的話,請登入看看權限夠不夠...

把這個再拿去包一層,所以真正的 controller 寫的大概是:

visitor_open(Album.find(params[:id]), :write){ |album|
@album = album
@photos = album.photos.paginate :page => params[:page]
}

這邊是用 block, 因為我的包法是如果有 block_given? 則把 block 給 granted.
另一個寫法則是指定 :granted => lambda{...} 和 :failed => lambda{...}.

需要注意的是,這個丟進來的 album 其實不是真正的 album, 而是一個 delegate.
(用 BasicObject in 1.9, ActiveSupport::BasicObject in 1.8)
我命名為 User::ResourcePerformer, 會針對所有操作做權限檢查,
還有改寫 methods call. 像是上面寫 album.photos 實際上不是呼叫 photos,
見下:

elsif resource.class.reflections[msg].ergo.macro == :has_many &&
msg.to_s.classify.constantize.reflections[:permission]
resource.__send__("#{msg}_for_visitor", performer, *args, &block)

如果呼叫的對像有 has_many 的關係,且存在 permission 的任何 association,
則改寫為 "#{msg}_for_visitor", 並把 visitor 丟進去。
所以呼叫 album.photos 實際上會變成 albums.photos_for_visitor(@visitor, ...)

除此之外,諸如 assignment, save, destroy, update_attributes 等也有檢查
write 權限。這是避免你用 visitor_open(album, :read) 然後卻去呼叫 save.
visitor_open 只是檢查會不會進入 block, 而不是其操作是否正確。
(不過其實如果用 :read 去開,我會呼叫 readonly! 所以可能也不用擔心)

當然這有缺陷,所以才會希望寫成真正的 granted 機制,不過不好寫...
所以就先用這種多重檢查的方式做看看。

檢查的方式也很簡單,只是呼叫 photo/album 的 readable_for? 和 writable_for?
至於那兩個怎麼寫,應該不用講了,很簡單,上一篇也有提到。



*

也就是說,現在的作法完全就是 photo/album 本身沒有權限檢查,
但是透過 visitor_open 就有。所以記得在 controller 裡面,
一定要用 visitor_open, 而不要直接操作 photo/album.

有了以上的東西,操作就可以變得很複雜。像是移動 photo 時,
先 visitor_open album, 在裡面:

massively_update_photos(params[:photo]){ |photo|
photo.move_out(album) if photo.to_move_out == '1'
}

這個 to_move_out 是一個 dummy value, 僅存在記憶體中,
在 update_attributes 時會自動寫入。
而 massively_update_photos 裡面,也要一張張 visitor_open,
以確保每一張照片使用者都有寫入權限。(不然自己 post 給 server 就能亂移動)

photos_hash.each{ |photo_id, attributes|
visitor_open(Photo.find_by_id(photo_id), :write){ |photo|
photo.attributes = attributes
photo.tag_list = TagList.from attributes[:tag_list]
# user should add tag list from all photos as well.
@visitor.tag_list.add attributes[:tag_list], :parse => true

yield(photo) if block_given?

photo.save
}
}

此外,visitor_open 會忽略 nil, 所以不用擔心 find_by_id 的結果會是 nil.
權限不足的就跳過即可,更新權限足夠的,要有一定的容錯能力。

in_place_editing 則也需要修改,加上可傳入的 predicate 判斷 writable_for?

至此,整個權限管控差不多就告一段落了....... 才怪 orz
password 忘記加上去了... 這邊應該要從原本 render_optional_error_file 著手,
在 session 中記住使用者曾經輸入過的 password, 每次重新判斷 password 是否正確。
如果這個架構寫得夠好,我相信幾小時內應該可以掛上 password 吧... 希望如此。

至於再加上 role 的機制,只好繼續往後延了。

寫程式之所以有趣,在於會不斷發現事情其實沒那麼單純(誤)

0 retries:

Post a Comment

All texts are licensed under CC Attribution 3.0