LoginSignup
4
3

More than 5 years have passed since last update.

UnixCryptな設定ファイルをRubyのCSV.tableで扱う (/etc/shadow や ftpd.passwd など)

Last updated at Posted at 2013-08-19

この記事は最終更新から1年以上経過しています。 気をつけてね。

ProFTPdのユーザ管理をChefのCookbookでしたいという話があって、リストのファイルを見てみると1行1レコード、コロン区切りのファイルでした。各フィールドは/etc/passwdと同じです。

通常ユーザ管理はCLI(ftpasswd)でやるとのことでしたが、どうせ中身が正しければちゃんと動くのでRubyで取り扱ってみます。

サンプルのftpd.passwdファイル

さて、サンプルのftpd.passwdを用意しました。
ユーザはhoge1-3の3名分、パスワードはそれぞれpassword1-3salted hash(SHA512)に加工しています。

ftpd.passwd
hoge1:$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1:99:99:hoge1-san:/home/hoge1:/sbin/nologin
hoge2:$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/.:99:99:hoge2-san:/home/hoge2:/sbin/nologin
hoge3:$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0:99:99:hoge3-san:/home/hoge3:/sbin/nologin

unix_cryptで認証文字列を生成するメソッド

RubyGems:unix_cryptをつかって、プレーンパスワードから/etc/shadowで見かける形式に変換するサンプルです。
記事中のサンプルで#build_encrypt_by_planeが出てきますがこのような処理をしています。

unix_crypt_example.rb
require 'openssl'
require 'unix_crypt'

def _make_salt_by_plane(password)
  OpenSSL::Digest::MD5.hexdigest(password + 'salt_me').slice(0,15)
end

def build_encrypt_by_plane(password, type = :SHA512) # [:MD5, :SHA256, :SHA512]
  UnixCrypt.const_get(type).build(password, _make_salt_by_plane(password))
end

saltはランダムで生成するべきなのですが、Chefなどで管理する際に毎回更新されて多少うっとおしくなる事を考慮して今回は固定文字列にしています。
ランダムかつ毎回更新されないようにするには、UnixCrypt.valid?($plainpass, $encrypted_strings)でパスワードが更新されているかのチェックを行うなど工夫の余地がありますね。

#build_encrypt_by_plane実行するとsalted hashを出力します。

Pry_build_encrypt_by_plane
pry(main)> build_encrypt_by_plane('password3')
=> "$6$f22eb55ffd0731f$FhoAmeAQmcqsGbEnRJWLuqv.AwlpYHiRz8xecGE.0teBnIY3pzko2y7lRl.rXcUraZVLJ4Kc.vF5EUU5HjTir0"

プレーンパスワード自体をどこに保管するかはそれなりに課題ですが、とりあえず棚上げしておきます。

CSV.tableとして読み込む

では各フィールドに名前をつけつつ、カラムの区切りにコロンを指定してftpd.passwdを読み込んでみます。

Pry_read_from_ftp_passwd
pry(main)> require 'csv'
=> true

pry(main)> table = CSV.table('ftpd.passwd', {:headers => ['name', 'encrypt', 'uid', 'gid' ,'info' ,'home' ,'shell'], :col_sep => ':'})
=> #<CSV::Table mode:col_or_row row_count:4>

pry(main)> table[0]
=> #<CSV::Row name:"hoge1" encrypt:"$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1" uid:99 gid:99 info:"hoge1-san" home:"/home/hoge1" shell:"/sbin/nologin">

pry(main)> table[1]
=> #<CSV::Row name:"hoge2" encrypt:"$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/." uid:99 gid:99 info:"hoge2-san" home:"/home/hoge2" shell:"/sbin/nologin">

pry(main)> table[2][:info]
=> "hoge3-san"

ファイルの各行がCSV::ROWのオブジェクトになって色々と取り回しやすくなりましたね。

表示する(Stringに変換する) [to_csv]

無事にRubyのオブジェクトとして取り込めたので、:write_headersオプションの有り無しで表示してみます。

Pry_show
pry(main)> puts table.to_csv(:write_headers => true, :col_sep => ':')
name:encrypt:uid:gid:info:home:shell
hoge1:$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1:99:99:hoge1-san:/home/hoge1:/sbin/nologin
hoge2:$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/.:99:99:hoge2-san:/home/hoge2:/sbin/nologin
hoge3:$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0:99:99:hoge3-san:/home/hoge3:/sbin/nologin
=> nil

pry(main)> puts table.to_csv(:write_headers => false, :col_sep => ':')
hoge1:$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1:99:99:hoge1-san:/home/hoge1:/sbin/nologin
hoge2:$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/.:99:99:hoge2-san:/home/hoge2:/sbin/nologin
hoge3:$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0:99:99:hoge3-san:/home/hoge3:/sbin/nologin
=> nil

加工後に保存する際の中身としても問題なさそうです。

探す [find, find_by_index]

ではCSV.tableから任意のレコードを探してみます。

ユーザ名から行Get

行を取得するには#findを使います。

Pry_find_by_name
pry(main)> user = table.find { |row| row[:name] == 'hoge2' }
=> #<CSV::Row name:"hoge2" encrypt:"$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/." uid:99 gid:99 info:"hoge2-san" home:"/home/hoge2" shell:"/sbin/nologin">

pry(main)> user[:encrypt]
=> "$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/."```

最初にヒットしたものをCSV::ROWとして利用できます。

目的ユーザの行インデックスを取得する

#find_indexではヒットしたレコードのインデックスを得られます。

Pry_find_by_index
pry(main)> table.find_index {|x| x[:name] == 'hoge3'}
=> 2

pry(main)> table[2]
=> #<CSV::Row name:"hoge3" encrypt:"$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0" uid:99 gid:99 info:"hoge3-san" home:"/home/hoge3" shell:"/sbin/nologin">

インデックスを使っても目的の行を取得することができます。

検索して更新する

先程の#find_indexを利用して、新しいパスワードを設定してみます。ユーザhoge3に対しパスワードをupdate_passwd3とします。

Pry_update
pry(main)> table[2]
=> #<CSV::Row name:"hoge3" encrypt:"$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0" uid:99 gid:99 info:"hoge3-san" home:"/home/hoge3" shell:"/sbin/nologin">

pry(main)> (table[table.find_index {|x| x[:name] == 'hoge3'}])[:encrypt] = build_encrypt_by_plane('update_passwd3')
=> "$6$fd75fc6a14847b3$CHMTRMkQg3WHqTh.6OrTiahpuPvnoO4IFWFRHWjepXujMdb8NcJFwqeuSH4OCcgi8jaZu.q1Ht7e13RK9YKwW."

pry(main)> table[2]
=> #<CSV::Row name:"hoge3" encrypt:"$6$fd75fc6a14847b3$CHMTRMkQg3WHqTh.6OrTiahpuPvnoO4IFWFRHWjepXujMdb8NcJFwqeuSH4OCcgi8jaZu.q1Ht7e13RK9YKwW." uid:99 gid:99 info:"hoge3-san" home:"/home/hoge3" shell:"/sbin/nologin">

encryptフィールドの中身が更新されました、行単位の更新も同じように可能です。

加える [<<]

新しいユーザを作成する要領です。

CSV::Rowのインスタンスを任意の内容で作成し、#<<でテーブルに行を追加します。
ユーザhoge4をパスワードpassword4で作成します。

Pry_find
pry(main)> new_row = CSV::Row.new(
  [:name,:encrypt,:uid,:gid,:info,:home,:shell],  
  ['hoge4', build_encrypt_by_plane('password4'), 99, 99, 'hoge4-san', '/home/hoge4', '/sbin/nologin']  
)  
=> #<CSV::Row name:"hoge4" encrypt:"$6$b0782018ad3bf4a$hGkXfKKV0wY0OM2KcvvhLs86izBDd46HEwf1ZgD0NIHdmj7n.J54S/p0IHQ43AVUlgyb/nLDexXIvYE1qjkRg0" uid:99 gid:99 info:"hoge4-san" home:"/home/hoge4" shell:"/sbin/nologin">

pry(main)> table << new_row
=> #<CSV::Table mode:col_or_row row_count:5>

pry(main> table[3]
=> #<CSV::Row name:"hoge4" encrypt:"$6$b0782018ad3bf4a$hGkXfKKV0wY0OM2KcvvhLs86izBDd46HEwf1ZgD0NIHdmj7n.J54S/p0IHQ43AVUlgyb/nLDexXIvYE1qjkRg0" uid:99 gid:99 info:"hoge4-san" home:"/home/hoge4" shell:"/sbin/nologin">

## 既に居るユーザなら追加しない
pry(main)> table << new_row unless table.find {|row| row[:name] == 'hoge4' }
=> nil

#pushで複数のレコードを同時に追加もできるようです。

削除する [delete, delete_if]

任意の行を削除するには#delete または #delete_ifです。
ユーザhoge2の行を削除します。

Pry_delete
pry(main)> table.delete table.find_index {|x| x[:name] == 'hoge2'}
=> #<CSV::Row name:"hoge2" encrypt:"$6$1a4a5969f81da2d$kT.LO1omkKavQs/kkh24ZnZO5o/FxlFfjVMverUr9iA4pL/iiEBCNwla.pLi3zlHAhCA4jr056msXD5CwAGk/." uid:99 gid:99 info:"hoge2-san" home:"/home/hoge2" shell:"/sbin/nologin">

pry(main)> table.delete table.find_index {|x| x[:name] == 'hoge2'}
=> [nil, nil]

pry(main)> puts table.to_csv(:write_headers => false, :col_sep => ':')
hoge1:$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1:99:99:hoge1-san:/home/hoge1:/sbin/nologin
hoge3:$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0:99:99:hoge3-san:/home/hoge3:/sbin/nologin
=> nil

# -------Reload table

pry(main)> table.delete_if {|row| row[:name] == 'hoge2'}
=> #<CSV::Table mode:col_or_row row_count:3>

pry(main)> puts table.to_csv(:write_headers => false, :col_sep => ':')
hoge1:$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1:99:99:hoge1-san:/home/hoge1:/sbin/nologin
hoge3:$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0:99:99:hoge3-san:/home/hoge3:/sbin/nologin
=> nil

#delete は indexを渡すと消してくれるんですが、消すとindexが詰められて普通にFixnumを渡すと冪等にならないのでfindを使っています。
#delete_ifは戻りのオブジェクトがCSV::Tableなので、対象のレコードがあってもなくても結果に違いがありません。

この点はちょっと注意が必要かもしれませんね。

書き出す

加工が終わったらファイルに書き出します。ChefならResource::FileまたはLWRPでやるところですがとりあえずFile.openで。
先程のuser2を消す処理を施した内容で出力してみます。

Pry_output_to_file
pry(main)> File.open('ftpd.passwd_new','w') {|f| f.write(table.to_csv(:write_headers => false, :col_sep => ':'))}
=> 308

pry(main)> .cat ftpd.passwd_new
hoge1:$6$17ed5828ed832c5$3KWXpvu9tyOF5UEGswbahSQVAYRMt7ZEoxiMOT./8or8ipRrpL090sD.AowBeFyDAEIxBhxzG4DA/nqnzhHWR1:99:99:hoge1-san:/home/hoge1:/sbin/nologin
hoge3:$6$031669a10a1ff3b$BNDb4nGS1cxGfJzKJntaegQZ8NkOKFKr2S6zJsyFO5Ph6TbD0nCMUtbIVN1KqPhUIItcLU35I2gHjs.16UGNn0:99:99:hoge3-san:/home/hoge3:/sbin/nologin

構成管理フレームワークを使わない場合は、元のファイルを事前にコピーなどしておくと良いかもしれませんな。

おわりに

使いやすいCRUDのインターフェイスがなかったり、ぱっと見よく分からんフォーマットの生ファイルも、そもそもそれを使うプログラムがパースしやすいようになっています。合わせてあげるのはそれほど難しいことではないのかもしれません。
なお、httpdの設定とかはおとなしくテンプレート使います。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3