ユーザーごとに閲覧を制限する。

全てのユーザーが平等に同じファイル内容を見られる現在の状態では、CSVサーバープロジェクトの意味は無い。CSVファイルをメールに添付して送信するのと同じことだ。目指すところは、ユーザーごとに設定した権限によって、閲覧内容を制限すること。この機能をどのように実現すべきか、ちょっと考えてみた。

まず、公開するCSVファイルに一行ごとに、誰が閲覧することが出来るか、その情報を付加する必要がある。ここでは、部署別に閲覧を制限することを考えた。CSVファイルに「path」というタイトルの1列を追加して、そこに閲覧可能な部署情報を付加することにした。

例えば、以下のような組織があったとして...

株式会社RAILS
    |---営業1部
    |       |---営業1課
    |       |---営業2課
    |       |---営業3課
    |
    |---営業2部
            |---営業1課
            |---営業2課

path列の閲覧情報には、以下のどれかを記述する。

1. /株式会社RAILS/
2. /株式会社RAILS/営業1部/
3. /株式会社RAILS/営業1部/営業1課/
4. /株式会社RAILS/営業1部/営業2課/
5. /株式会社RAILS/営業1部/営業3課/
6. /株式会社RAILS/営業2部/
7. /株式会社RAILS/営業2部/営業1課/
8. /株式会社RAILS/営業2部/営業2課/

もし、ユーザーが /株式会社RAILS/営業2部/ の閲覧権限を持っていたとしたら、そのユーザーはpath列に6、7、8の組織のパスが書いてある行を閲覧できる。つまり営業2部以下の組織は全て閲覧できるのだ。ユーザーが社長であれば、その権限は /株式会社RAILS/ であるべきだ。そうすれば1〜8まで、全ての組織を閲覧することが出来る。
特殊な条件として、path列がブランクの行は、だれでも閲覧できることにした。また、閲覧権限を一つも持たないユーザーは、path列がブランクの行だけ閲覧できることになる。以上のような仕様で閲覧制限してみた。

LoginEngineの利用

ユーザー別に管理するので、LoginEngineを利用することにした。以前のsoftwarebookプロジェクトで利用した日本語化したものがあるので、そのままコピーして使うことにした。

  • enginesフォルダ、login_engineフォルダをvender/pluginsフォルダにコピーして、db:migrate:enginesを実行した。
  • Ruby-GetTextで日本語化したLoginEngineの設定は以下のようにしてある。
# ----------config/environment.rb----------
$KCODE = 'u'
require 'jcode'
# Be sure to restart your web server when you modify this file.

...(途中省略)...

# Include your application configuration below
require 'gettext/rails'

module LoginEngine
  config :salt, "zarigani"
  # 今回、メールによる認証はしない設定にした。
  config :use_email_notification, false
  # 以下で編集可能なフィールドを設定している。
  config :changeable_fields, [ 'firstname', 'lastname' ]
end

Engines.start :login
# ----------app/controllers/application.rb----------
require 'login_engine'

class ApplicationController < ActionController::Base
  init_gettext "csv_server"
  
  include LoginEngine
  helper :user
  model :user
  
  before_filter :login_required
end
# ----------app/helpers/application_helpers.rb----------
module ApplicationHelper
  include LoginEngine
end

path、paths_usersテーブルの追加

閲覧制限をかける組織情報は、/株式会社RAILS/営業1部/営業1課/ のような書式でpathsテーブルに保存しておく。また、usersテーブルとは多:多の関連になるので、paths_usersテーブルも作成した。

  • まずはモデルの作成。
script/generate model path
  • テーブルを追加するマイグレーションファイルは以下の通り。
  • :paths_usersテーブルは、多:多関連を設定するための実体(モデル)のないテーブル。
  • idフィールドを無しにするときは、:id => false とオプションをセットする。
# ---------- db/migrate/003_create_paths.rb ----------
class CreatePaths < ActiveRecord::Migration
  def self.up
    create_table :paths do |t|
      t.column :name, :string
    end
    
    create_table :paths_users, :id => false do |t|
      t.column :path_id, :integer
      t.column :user_id, :integer
    end
  end

  def self.down
    drop_table :paths
    drop_table :paths_users
  end
end

多:多の関連の設定

usersテーブル、pathsテーブルの関連を設定した。モデルの設定は以下の通り。(オレンジ色の部分が追記した箇所)

# ---------- app/models/user.rb ----------
class User < ActiveRecord::Base
  include LoginEngine::AuthenticatedUser

  untranslate :salt, :salted_password, :verified, :role, :security_token,
              :token_expiry, :created_at, :updated_at, :logged_in_at,
              :deleted, :delete_after

  N_("User|Password")

  has_and_belongs_to_many :paths
end
# ---------- app/models/path.rb ----------
class Path < ActiveRecord::Base
  has_and_belongs_to_many :users
end

with_scopeで閲覧制限をする。

閲覧制限は、with_scopeでやってみた。主要なコードは以下のようになった。

...(途中省略)...  
  # 閲覧制限をかけるため、with_scopeのオプション設定を返す。
  # 例:ログインユーザーが、営業1部1課を担当している場合、以下のようなハッシュを作成して返す。
  # {:find => {:conditions => path LIKE '/株式会社RAILS/営業1部/営業1課/%' OR path=''}
  def login_user
    sql = current_paths.inject("path=''") do |result, item|
            "path LIKE '#{item}%' OR " + result
          end
    {:find => {:conditions => sql}}
  end
  
  # ログインユーザーが所有しているpathsテーブルのnameフィールドの配列を返す。
  def current_paths
    paths = session[:user].paths.map(&:name)    
  end
...(途中省略)...
  def component
  Display.with_scope(login_user) do
    @show_wrapper = true if @show_wrapper.nil?
    @sort_sql = Display.scaffold_columns_hash[current_sort(params)].sort_sql rescue nil
    @sort_by = @sort_sql.nil? ? "#{Display.table_name}.#{Display.primary_key} asc" : @sort_sql  + " " + current_sort_direction(params)
    @paginator, @displays = paginate(:displays, :order => @sort_by, :per_page => default_per_page)
    
    render :action => "component", :layout => false
  end
  end
...(途中省略)...

ユーザーを閲覧制限情報のpathと関連付けて登録する。

userとpathを関連付けるビューは、以下のようにした。path.nameの一覧にチェックボックスを付けて表示するだけ。今の所なんの工夫もない...。本当はツリー表示で表現したいところだ。

<%# ----------app/views/user/_edit.rhtml---------- %>
<div class="user_edit">
  <table>
    <%= _("%{firstname} %{lastname}") % {
          :firstname => (form_input changeable(user, "firstname"), _("First Name"), "firstname"),
          :lastname => (form_input changeable(user, "lastname"), _("Last Name"),"lastname")} %>
    <%= form_input changeable(user, "login"), _("Login ID"), "login", :size => 30 %><br/>
    <%= form_input changeable(user, "email"), _("Email"), "email" %>
<!--path情報をチェックボックスでリスト表示-------------------------------------------->
    <tr>
      <td><label for="path_name">Path:</label></td>
    </tr>
    <% for path in Path.find(:all) %>
      <% checked = @user.paths.find(path.id) rescue nil %>
      <tr>
        <td></td>
        <td><%= check_box_tag "checked_items[#{path.id}]", path.id, checked %><%=h path.name %></td>
      </tr>
    <% end %>
<!--------------------------------------------path情報をチェックボックスでリスト表示-->    
    <% if submit %>
      <%= form_input :submit_button, (user.new_record? ? _('Signup') : _('Change Settings')), :class => 'two_columns' %>
    <% end %>
  </table>
</div>

チェックボックスの情報を受け取るコントローラーは、オレンジ色の部分を以下のように追記した。

# ---------- app/controllers/user_controller.rb ----------
...(途中省略)...
  def signup
    return if generate_blank
    params[:user].delete('form')
    params[:user].delete('verified') # you CANNOT pass this as part of the request
    @user = User.new(params[:user])
    begin
      User.transaction(@user) do
        @user.new_password = true
        unless LoginEngine.config(:use_email_notification) and LoginEngine.config(:confirm_account)
          @user.verified = 1
        end
        if @user.save
          @user.paths = Path.find(params[:checked_items].keys) if params[:checked_items]
...(途中省略)...

  protected
    def do_edit_user(user)
      begin
        User.transaction(user) do
          user.attributes = params[:user].delete_if { |k,v| not LoginEngine.config(:changeable_fields).include?(k) }
          if user.save
            # path情報を一つも持たない状態を登録するため、対応するユーザーを、全削除してから登録する。
            user.paths.delete(user.paths)
            user.paths = Path.find(params[:checked_items].keys) if params[:checked_items]
...(途中省略)...


以上で、閲覧制限の準備は整った。新規ユーザー登録の画面は以下のようになる。


以下のような表を作成して、CSVファイルに書き出す。

    • 利益管理の最小単位は、分類コードと呼ばれる3桁の数字で表現されるコード。
    • 組織は、この分類コードを所有して構成される仕組みにする。


/株式会社RAILS/にチェックを入れた状態での表示はこうなる。


下記のように、二つの組織にチェックを入れた状態に変更すると...

    • /株式会社RAILS/営業1部/営業1課/
    • /株式会社RAILS/営業2部/営業1課/

チェックを入れた組織だけが表示される!