Authlogicに関するメモ
認証機能再発明するべからずは有名な鉄則ですが、まるで知らないままというのもマズイので実装を読むことでお茶を濁していきたい。
salt
やSCrypt
の運用、persistence_token
の更新などで比較的安全にいけるのでは。
パスワード
salt
Authlogic::Random.friendly_token
def friendly_token # use base64url as defined by RFC4648 SecureRandom.base64(15).tr('+/=', '').strip.delete("\n") end
暗号化
authlogic/lib/authlogic/crypto_providers
以下に、各種Cryptorをラップしたアダプターがある。
SCrypt
ならAuthlogic::CryptoProviders::SCrypt#encrypt(*tokens)
で暗号化パスワードを得られる。
*tokens
には[password, salt]
が入り、salt
は保存されてログイン時のパスワード一致を見る時に使われる。
生パスワードは勿論破棄される。
パスワード確認
各種Cryptorの比較演算子を使ったりする。
# ユーザーのレコードから得る salt = user.password_salt encrypted = user.encryped_password # 入力 password = 'aaaaaaaa' # 比較 SCrypt::Password.new(encrypted) == Authlogic::CryptoProviders::SCrypt.encrypt(password, salt)
セッション
cookie
などの値を得るために、Controller
インスタンスを持つアダプターが必要になる。
Controller
経由では、初期化時に自動的に付与されるAuthlogic::ControllerAdapters::RailsAdapter
が設定する。
Authlogic::Session::Base.controller = RailsAdapter.new(self)
それ以外で使いたい場合は、必要な値を返すなにかを用意する。
class ControllerLike def initialize(request) @request = request end def cookies @request.cookies end def params @request.params end def session @request.session end def responds_to_last_request_update_allowed? true end def last_request_update_allowed? false end def method_missing(*args) false end end Authlogic::Session::Base.controller = ControllerLike.new(request)
開始
create
時にユーザーレコードにpersistence_token
を保存する。
クッキーにはそのpersistence_token
とユーザーのid
を、後にsplit
可能な形で保存する。[persistence_token, id].join('::')
など。
復元
保存しておいたクッキーの値のid
からユーザーを検索、persistence_token
と一致するか見る。
AWS Lambdaで使う関数をローカルでテストするサーバーを建てる。
AWS Lambda + AWS Api Gateway + AWS DynamoDBでなにかをつくることにはまっています。
AWS Lambdaで使う関数自体はmochaなどでテストできますが、実際にブラウザから叩くテストをローカルでしたいと思いました。
そこでNode.jsで簡単なサーバーを建てます。
まず、AWS Lambdaはこのような関数です
"use strict"; const Workbook = require('./src/workbook').Workbook; const Marker = require('./src/marker').Marker; exports.handler = (data, context, callback) => { let workbook = new Workbook(data); let marker = new Marker(workbook, data); marker.mark((result) => { if (result) { context.succeed({result: 'correct', data: marker.explain()}); } else { context.fail(JSON.stringify({result: 'incorrect', data: marker.explain()})); } }); };
Workbook
やMarker
がDynamoDBにアクセスします。環境変数を使って、内部で参照ファイルを切りかえ、ローカルのDynamoDBにアクセスするようにしているので、そちらは大丈夫です。
テストサーバーです
サーバー側の言語をまともに触ったのはNode.jsがはじめてで、はじめて書いたのもこのような簡易なサーバーでした。非常に懐かしいですね。
import * as http from 'http'; import * as qs from 'querystring'; import * as marker from '../marker/index'; http.createServer(function (req, res) { if (req.method !== 'POST' && req.method !== 'OPTIONS') { res.end(''); return; } res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST'); res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); res.setHeader('Content-Type', 'application/json'); if(req.method === 'OPTIONS'){ res.end(``); return; } let body = ''; req .on('data', (data) => { console.log('data', data); body += data }) .on('end', () => { qs.parse(body); marker.handler(JSON.parse(body), { succeed: (data)=> { res.end(JSON.stringify(data)); }, fail: (stringifiedData)=> { res.end(stringifiedData); } }); }); }).listen(process.env.PORT || 3080, '127.0.0.1');
OPTIONS
によるプリフライト対応処理を追加しました。(昨日は素のPOST
だけ飛んでたのにな……)
まずPOST
とOPTIONS
以外はハネます。
つぎに、データの整形ですね。こういうのは自動でやってくれないんでしょうか。
marker.handler
がAWS Lambdaでの関数本体なので、そこへデータを投げます。
context.succees
とcontext.fail
で関数を終了するつくりなので、そこに関数を妖異します。その関数呼び出しをもってJSONレスポンスを作成します。
実際のfail
では{errorMessage: 引数}
というレスポンスになりますが、これはAWS Api Gatewayで整形します。ので、それを踏まえたJSONとします。
$ TEST=true babel-node test-server/index.js
テストできた
これでブラウザ側からSuperagentなどを使いPOST
すると、実際のレスポンスが得られるようになりました。
クリッククリックで行うテストがはかどりますね。
`Regexp`などActiveRecordでメソッドが用意されていない標準SQLの演算子を清く正しく使う。
清く正しくとは、生SQL文字列を書かない程度の意味です。
下の方の長いやつはRails 4
までの話です
REGEXP
はRails 5
もしくはArel 7
からmatches_regexp
としてメソッドが用意されました。
Writer.where(Writer.arel_table[:email].matches_regexp('.*@gmail.com')) # SELECT "writers".* FROM "writers" WHERE ("writers"."email" ~ '.*@gmail.com')
Rails 5
になってなお下のような書き方をしていると殴られる可能性があります。
このようにどんどん便利になっていってるので、たまに知識更新するのも大切ですね。
たとえばRegexp
でwhere
する
グーグル先生にたずねるとStack Overflowが出てきて以下のようなコードが出てきます。
Writer.where("email REGEXP ?", '.*@gmail.com') # SELECT "writers".* FROM "writers" WHERE (email ~ '.*@gmail.com')
動きますが、せっかくのORMですから、ORMらしく書きましょう。
だいたいの演算子や句はArel
に用意されている
ドキュメントやグーグル先生より、ソース内を検索したほうが早いのですが、大体の演算子のビルダーが用意されています。
REGEXP
なら以下のように書けます。
Writer .where( Arel::Nodes::Regexp.new( Writer.arel_table[:email], Arel::Nodes::Casted.new('.*@gmail.com', Writer.arel_table[:email]) ) ) # SELECT "writers".* FROM "writers" WHERE "writers"."email" ~ '.*@gmail.com'
WHERE
句のカラム指定にテーブル名が入りました。
より具体的な記述になっていますが、テーブル名が欲しくない場合もあります('AS'した場合とか)。
ActiveRecord#arel_table
で得られるArel::Table
はその名の通りテーブルを前提とした値しか返してくれませんので、自分で組みたてる必要があります。
Writer .where( Arel::Nodes::Regexp.new( Arel.sql(Writer.connection.quote_column_name(:email)), Arel::Nodes::Quoted.new('.*@gmail.com') ) ) # SELECT "writers".* FROM "writers" WHERE "email" ~ '.*@gmail.com'
カラムのクォーティングのルールはデータベースによって違うので、quote_column_name
で万全を期すと良いでしょう。
値は値用にクォーティングします。
WHERE
句の()
が欲しい場合はさらにこうします。
Writer .where( Arel::Nodes::Grouping.new( Arel::Nodes::Regexp.new( Arel.sql(Writer.connection.quote_column_name(:email)), Arel::Nodes::Quoted.new('.*@gmail.com') ) ) ) # SELECT "writers".* FROM "writers" WHERE ("email" ~ '.*@gmail.com')
大変な騒ぎになってきましたね。
Arel
のルール
Arel
は安全性を維持するために、生のString
は受けつけてくれず、なんらかのArel
クラスでラップする必要があります。
ところでArel.sql
はArel::Nodes::SqlLiteral.new
のショートカットで、これは何の評価も加工もされず、SQLに渡ります。
カジュアルに使うと生SQLをいじくりまわしているのと大して変わらないので気をつけましょう。
長い
記述が長くなってうれしみが少ないですか?
最初はウッとなりましたが、自分で文字列を組みたてるのと比べると、これが別に気にならなくなったりします。
フシギですね。
AWS Lambdaを使って、ブラウザ側とサーバー側で同じバリデーションをするということをやった。
日記です。今日の日記コードはこれ。
qiitaにはさすがにもうAWS Lambdaでフォームなんていう記事は山盛りあったのでこっちで。
日記
Node.jsはサーバーとして動かせるので、ブラウザ側とサーバー側で同じスクリプトを用いることが可能です。(アイソモーフィック?)
しかし動かすとなるとサーバーを用意して〜、など色々と難関がありました。その難関を軽く解消してくれたのがAWS Lambdaであることはみなさんご存知のとおりですね。
バリデーションの準備
まずバリデーターライブラリを用意します。
これにはvalidator
を使いました。
これをラップしたRecord
というクラスをつくり、以下のようなコンフィグ(config.jsに入っています)をあたえ、validate
メソッドで確認できるようにしました。
exports.config = { attributes: ['name', 'email', 'age', 'gender'], validation: { name: { isLength: {min: 1, max: 20, message: '1-20文字で入力してください'} }, email: { isEmail: {message: 'メールアドレスを入力してください'} }, age: { isNumeric: {message: '数字を入力してください'} }, gender: { isIn: ['female', 'male', 'other'] } } };
ブラウザ側バリデーション
あとはこのバリデーターを、ブラウザ側で使えるようにします。
ライブラリやコンフィグファイルが別ファイルになっているので、毎度おなじみのbrowserify
で連結します。
const Record = require('../lib/src/record').Record; const config = require('../lib/src/config').config; const record = new Record(config); const Validator = window.Validator = {}; Validator.validate = (data) => { if (record.assign(data).validate()) { return {result: 'success', data: record.parameters}; } else { return {result: 'failure', errors: record.errors}; } };
あとはこのバリデーションをサーバーに送信する前に行い、validであれば送信します。
サーバー側バリデーション
送信されてくるデータは全く安全でないのは常識ですから、サーバー側でもバリデーションする必要があります。
ブラウザ側とほぼ同じコードですが、AWS LambdaではNode.jsが動いていますから、browserify
は必要ありません。
今回はバリデーションの成否で終了していますが、そこらへんにdynamoDBやメール送信の処理を書けばいいんじゃないでしょうか。
"use strict"; const Record = require('./src/record').Record; const config = require('./src/config').config; exports.handler = (event, context, callback) => { let record = new Record(config); if (record.assign(event).validate()) { context.succeed({result: 'success', data: record.parameters}); } else { context.fail(JSON.stringify({result: 'failure', errors: record.errors})); } };
エラー時の戻り値がブラウザ側と違ってしまうのが気になるところですが(fail
の引数は{"errorMessage": 引数}
なJSON
として飛んでいく)、これはAWS Api Gateway側でfailureをキャッチして、同じ形にします。
.*"result":"failure".*
で失敗ステータスコードに振り、本文マッピングテンプレートでエラー部を取り出したものをレスポンスとします。
$input.path('$.errorMessage')
ブラウザ側送信時処理
送信せずとも同じバリデーションが行えるわけですから、送信前にバリデーションし、invalidであればdeployErrors
というメソッドが、各項目のinput
の上や下にエラーを表示します。
もし何らかの都合でinvalidなデータが送信されても、サーバー側でinvalid判定されて、同じエラーが返ってきてdeployErrors
というメソッドが、各項目のinput
の上や下にエラーを表示します。
function send($form, $thanks) { var data = traceValue($form); var validated = Validator.validate(data); if(validated.result == 'failure'){ deployErrors($form, validated.errors); return; } disablizeButton($form, true); superagent .post(endPoint) .set('Accept', 'application/json') .send(data) .end(function (err, res) { if (err) { deployErrors($form, res.body.errors); disablizeButton($form, false); } else { $form.hide(); $thanks.show(); } }); }
わりと長年の夢であった、サーバーとクライアントで同じコードでバリデーションするというのが、比較的簡単にできてしまいました。しかも課金の心配が結構軽い。
フォーム作成というこまごまとした仕事は、前職前前職ともによくあって、cgiの、メンテされてるんだがされてないんだかわからないスクリプトを使用していました。
Rails大好きなわたしもさすがに一個の問い合わせフォームにRailsというわけにもいかなくて難儀していましたが、これならヤバイ脆弱性も自分の責任のうちで作成できるのでいいかもしれませんね。
HerokuにSymbolic Link入りを投げるとき気をつけること
リソースを再利用しようとしました。
そこでディレクトリに対してln -s
で作成しますが、例えば同ディレクトリ内の何がしかへ張る場合、
$ ln -s development staging
とする必要があり
$ ln -s ./development staging
これだと辿れなくなります。
セキュリティかなにかの都合でスラッシュアクセスがブロックされているのでは。同じ理由で上には登れないんじゃないかな(試してない)。
おうちでLinuxOS使っててもお外に発射したら動かないコードあるし世の中は厳しい。
— おふくろさま (@o296sm) 2016年7月3日
レールズにプルリクエストがマージされてハッピーだった
ので、思いつく限りのSNSやブログに書いています。
ここにも書きます。
どれ
以下の記事のやつを作ってる時に「バグかしら?」と思ってモンキーパッチで回避してて、せっかくだからとpull requestを飛ばしてみたら忘れた頃にマージされたという話です。
Rails5.0.0がつい先日リリースされましたが、その時には自分でも忘れていて、ほん今日、なんか通知が出てるなと思って見てみたらマージされていました。
無職してた甲斐があった
仕事をしないで自分で思うがままにコードを書きつづけていたところで出会ったバグ的なものでしたから、無職でなかったら出会わなかった可能性があります。
これがここのコードが原因で起こってる、っていうのも結局p
メソッドとクリッククリックで追いつめて見つけたみたいなもんですから、よっぽど時間に余裕がないと無理だったでしょう。
無職時代のいい思い出になったな〜〜と言いたいところですが、まだ就職先は決まっておりません。
大変ですね。
まぁとにかく嬉しい
ここ3年ぐらいで一番うれしいんじゃないの。
— おふくろさま (@o296sm) 2016年7月4日
好きなフレームワークに貢献できたということで、本当に嬉しいですね。
Discourseにみる権限管理。
権限管理に興味があって、ちょろちょろ読んでいます。
Spreeを読む前はDiscourseを読んでいました。Discourseでは権限管理にこれといったGemは用いず、独自の権限管理機能を実装しています。
Guardian
というクラスです。
ただの感想みたいになったのでこっちのブログにメモとして残します。
Guardian
用法はCancancanと大きくかわるところはありません。
まずcurrent_user
に類するものでGuardian
のインスタンスを作成します。
app/controllers/application_controller.rb
def guardian @guardian ||= Guardian.new(current_user) end
そしてそのインスタンスに処理内容を冠したメソッドに処理対象のなんらかのオブジェクを渡すと権限の確認が行われます。
app/controllers/users_controller.rb
guardian.ensure_can_edit_username!(user)
許可された動作ならそのまま処理はすすみ、許可されていない動作であればDiscourse::InvalidAccess
例外を発生します。
設定
Spreeにおいて、Cancancanはユーザー単位で権限を設定していました。
Guardian
は処理対象のオブジェクトのクラスごとにモジュールを用意します。
lib/guardian/user_guardian.rb
module UserGuardian # 略 def can_edit_username?(user) return false if (SiteSetting.sso_overrides_username? && SiteSetting.enable_sso?) return true if is_staff? return false if SiteSetting.username_change_period <= 0 is_me?(user) && (user.post_count == 0 || user.created_at > SiteSetting.username_change_period.days.ago) end # 略 end
そしてGuardian
にinclude
するというシンプルな仕組みです。
lib/guardian.rb
class Guardian include EnsureMagic include CategoryGuardian include PostGuardian include TopicGuardian include UserGuardian include PostRevisionGuardian include GroupGuardian #略 end
実行
前述の例のように接頭辞としてensure_
をつけた場合、設定されたメソッドがありません。
app/controllers/users_controller.rb
guardian.ensure_can_edit_username!(user)
そこでEnsureMagic
というmodule
に実装されたmethod_missing
があらためてメソッドを探索し、その結果をうけてtrue
を返すか例外を発生します。
感想
最初は単一のクラスで、その後どんどん増改築した結果、基本となるGuardian
にもわりとコードがありつつ、include
するmodule
にもコードがもりもりあるという感じになっています。
それを除けば、method_missing
から定義済みメソッドを探索して実行、権限を確認するという動作は、メソッドにより権限をドバっと設定するCancancanより好ましさを感じます。
自分でも
引数でわたされるオブジェクトのクラスを見て該当のGuardian
を検索してインスタンス化、そこからメソッドを探索して権限確認という実装を自分でも書いてみましたが、なかなかいい塩梅でした。
でも多分仕事でつかうならCancancanを使いますね。