[mruby] RedmineのユーザでBasic認証

ngx_mrubyを使ってRedmineのユーザでBasic認証してみるというお話。
ngx_mrubyのビルド方法や前提となるパッケージのインストールは割愛しています。

# mruby

## 必要なmrbgem

ngx_mrubyのbuild_config.rbに以下を追加してビルドします。
mysql-develやらmariadb-develやらが必要です。

```rb:build_config.rb
conf.gem :github => 'mattn/mruby-mysql' # コメントアウトを外す
conf.gem :github => 'mattn/mruby-base64' # WWW-Authenticateヘッダのデコード用
conf.gem :github => 'pbosetti/mruby-merb' # database.ymlのテンプレート解釈用
conf.gem :github => 'AndrewBelt/mruby-yaml' # database.ymlのパース用
```

## mruby_init_workerハンドラ

Worker起動時にDBコネクションをプーリングしておきます。

```rb:init_worker_hander.rb
REDMINE_HOME = ENV['REDMINE_HOME'] || '/var/lib/redmine'
RAILS_ENV = ENV['RAILS_ENV'] || 'production'

database_yml = File.read("#{REDMINE_HOME}/config/database.yml")

# database.ymlの内容をparse
merb = Merb.new
database_settings = YAML.load(merb.convert(database_yml))

config = database_settings[RAILS_ENV]

# DB接続をUserDataに格納 (同一Woker内で使い回す)
u = Userdata.new("dbconn_#{Process.pid}")
u.connection = MySQL::Database.new(
  :host => config["host"],
  :port => config["port"],
  :database => config["database"],
  :username => config["username"],
  :password => config["password"],
  :encoding => config["encoding"]||'utf8')
```

## mruby_exit_workerハンドラ

DBコネクションを破棄します。

```rb:exit_worker_hander.rb
# UserDataに保持していたDB接続を閉じる
Userdata.new("dbconn_#{Process.pid}").connection.close
```

## 本題のmruby_access_handlerハンドラ

Redmineのユーザテーブルを参照してパスワード認証を行います。

こちらの記事を大いに参考にさせていただきました。
[ngx_mruby で Basic 認証を実装する](http://qtakamitsu.hatenablog.jp/entry/2015/02/22/212325)

```rb:basic_auth_by_redmine.rb
QUERY = "select salt,hashed_password from users where type='User' and status = '1' and login=$1"

# Basic authorizetion
def basic_auth

  r = Nginx::Request.new
  realm = r.var.realm
  
  if r.headers_in["Authorization"].nil?
    r.headers_out["WWW-Authenticate"] = %Q(Basic realm="#{realm}")
    return Nginx::HTTP_UNAUTHORIZED
  end

  auth = r.headers_in["Authorization"]
  params = Base64::decode(auth.split(" ")[1]).split(":")
  id = params[0]
  passwd = params[1]

  # Proc call
  unless yield(id, passwd)
    r.headers_out["WWW-Authenticate"] = %Q(Basic realm="#{realm}")
    return Nginx::HTTP_UNAUTHORIZED
  end

  # OK
  return Nginx::DECLINED
end

# SHA1 digest
def sha1(clear_text)
  Digest::SHA1.hexdigest(clear_text||"")
end

def authenticate(id, passwd)
  # UserDataからコネクションを取得
  mysql = Userdata.new("dbconn_#{Process.pid}").connection
  row = mysql.execute(QUERY, id)
  if row.eof?
    row.close
    return false
  end
  cols = row.next
  salt = cols[0]
  hash = cols[1]
  row.close
  return sha1("#{salt}#{sha1(passwd)}") == hash
end

Nginx.return basic_auth {|id, password|
  authenticate id, password
}
```

# Nginx側の設定

```nginx:/etc/nginx/nginx.conf
# ... 略 ...
http {
  mruby_init_worker /path/to/init_worker_hander.rb cache;
  mruby_exit_worker /path/to/exit_worker_hander.rb cache;
  # ... 略 ...
  server {
    location / {
      set $realm 'Secure Zone';
      mruby_access_handler /path/to/basic_auth_by_redmine.rb cache;
    }
  # ... 以下略 ...
```

環境変数を設定してみる

Redhat系

```bash:/usr/lib/systemd/system/nginx.service
# 以下を[Service]セクションに追加
Environment=REDMINE_HOME=/var/lib/redmine
Environment=RAILS_ENV=production
```

もしくは

```bash:/etc/default/nginx
REDMINE_HOME=/var/lib/redmine
RAILS_ENV=production
```

# 何が嬉しい？

Basic認証に対応した製品(Jenkinsとか)にRedmineのユーザでSSOできます。
ついでにRedmineにredmine_http_authプラグインを導入しておけばログイン画面をスキップできます。
他にも静的ファイルに認証かけたり、SSOの用途でいろいろあるかも。思いつかないけど。
Basic認証なんでセキュリティとか気をつけて下さい。

こちらからは以上です。

# [補足] PostgreSQLの場合 (差分のみ)

```bash
sudo yum install postgresql-devel
```

```rb:build_config.rb
# conf.gem :github => 'mattn/mruby-mysql'
conf.gem :github => 'authorNari/mruby-pg'
```

```rb:init_worker_hander.rb
u = Userdata.new("dbconn_#{Process.pid}")
u.connection = PG::Connection.new(
  :host => config["host"],
  :port => config["port"],
  :dbname => config["database"],
  :user => config["username"],
  :password => config["password"])
```

```rb:basic_auth_by_redmine.rb
def authenticate(id, passwd)
  pg = Userdata.new("dbconn_#{Process.pid}").connection
  res = pg.exec(QUERY, [id])
  return false if res.nil? or res.size <= 0
  salt = res[0]["salt"]
  hash = res[0]["hashed_password"]
  return sha1("#{salt}#{sha1(passwd)}") == hash
end
```

init_workerでdatabase.ymlのadapterを見て分岐してもいいかもしれないけど、過度な抽象化な気がするので割愛。