satoshi.inoue's blog

備忘録を兼ねているので、薄い内容の投稿もあります。

Dependabotで依存パッケージの更新時に自動でPRを発行する

app.dependabot.com

GitHubと連携する

Sign in with GitHubから連携する。 f:id:acquapazza:20190529030214p:plain

Dependabotの監視対象とするリポジトリを選択する

f:id:acquapazza:20190529030231p:plain

複数のリポジトリを選択した場合は、チェックボックスにチェックを入れてAdd selectedをクリックして監視を開始する f:id:acquapazza:20190529030247p:plain

自動的にPRが発行される

対象のリポジトリの依存ライブラリに更新があった場合に、自動的にPRが発行される

go.mod, go.sumに更新がある

マージしたときのコミットログ

commit ee248ea8e8711cd1f6ac42dcc54d405eea241335
Author: dependabot-preview[bot] <dependabot-preview[bot]@users.noreply.github.com>
Date:   Tue May 28 08:50:04 2019 +0000

    Bump github.com/volatiletech/sqlboiler

    Bumps [github.com/volatiletech/sqlboiler](https://github.com/volatiletech/sqlboiler) from 3.3.0+incompatible to 3.4.0+incompatible.
    - [Release notes](https://github.com/volatiletech/sqlboiler/releases)
    - [Changelog](https://github.com/volatiletech/sqlboiler/blob/master/CHANGELOG.md)
    - [Commits](https://github.com/volatiletech/sqlboiler/compare/v3.3.0...v3.4.0)

まとめ

依存ライブラリの更新確認はDaily, Weekly, Monthlyの3つから選択できます。 自動的にgo,sum、go.modを更新してPRを発行してくれるので、かなり便利です。 個人の開発でも使えるので、積極的に使っていこうと思います。

PostgreSQLでテーブルロックせずにindexを貼る

通常、テーブルにindexを貼るときはCREATE INDEXを使うと思います。 私も基本的にはこれを使いますが、CREATE INDEXをオプションなしで使用する場合は注意が必要です。

www.postgresql.jp

注意点

PostgreSQLは、対象テーブルに対する書き込みをロックしてから、対象テーブル全体のインデックス作成を一度のスキャンで行います。
他のトランザクションはテーブルを読み取ることはできますが、対象テーブル内の行を挿入、更新、削除しようとすると、インデックス作成が完了するまでブロックされます。
ですので、運用状態にあるデータベースには重大な影響を与える可能性があります。
また大規模なテーブルに対してインデックスを作成する場合は何時間もかかることがあります。

CONCURRENTLYオプションとは

簡単に言うと、このオプションを使うことで 書き込みをロックしないインデックス作成 が行えます。
その為、インデックス作成中に通常の操作を行い続けることができるので、この方式は運用環境での新規インデックス作成に有用です。
ある時点のスナップショットを元にインデックスを作成し、インデックスへの挿入を有効化してから、改めて取ったスナップショットとの差分をインデックスに反映するというものです。

CONCURRENTLYオプションのデメリット

作業時間の増加

このオプションを使うと、PostgreSQLはテーブルを2回スキャンします。
さらに、潜在的にそのインデックスを更新または使用する可能性がある、実行中のすべてのトランザクションが終わるまで待機しなければなりません。
したがって、この方式は通常の方式よりも総作業時間がかかり、また、完了するまでの時間が非常に長くなります。
もちろん、インデックス作成によりCPUや入出力に余分に負荷がかかりますので、他の操作に影響を与える可能性があります。

無効なインデックスが残る可能性

デッドロックや一意性インデックスにおける一意性違反など、テーブルスキャン中に問題が発生すると、CREATE INDEXは失敗しますが、「無効な」インデックスが残ってしまいます。
こうしたインデックスは完全ではない可能性がありますので、問い合わせの際には無視されます。
しかし、更新時にオーバーヘッドがかかります。
psqlの\dコマンドでこうしたインデックスを検出できます。

postgres=# \d tab
       Table "public.tab"
 Column |  Type   | Collation | Nullable | Default 
--------+---------+-----------+----------+---------
 col    | integer |           |          | 
Indexes:
    "idx" btree (col) INVALID

こうなった場合の推奨方法は「インデックスを削除し、再度CREATE INDEX CONCURRENTLYを実行することです。」と公式が言っているんですが、長時間かかった挙句、またやり直すのかと思うとゾッとしますね。

まとめ

運用状態にある本番に影響を与えないためにCONCURRENTLYは実装されたはずなので、上記で述べたことが問題にならないのであれば、どんどん使っていった方がいいと思います。 CONCURRENTLYはHeap Only Tupleの影響を受けるようなので、後日Heap Only Tupleについて調査しようと思います。

PKCE: OAuth2.0の認可コード横取り攻撃への対策

これの続き。 www.acquapazzablog.com

認可コード横取り攻撃とは

Authorization Code Flowで認可コードを発行して、ブラウザから元のアプリにカスタムURLスキーム等で戻ってくるときに、攻撃者によってインストールされたプログラム等により、認可コードを抜き取られる攻撃です。

PKCEとは

OAuth 2.0 public clients utilizing the Authorization Code Grant are susceptible to the authorization code interception attack. This specification describes the attack as well as a technique to mitigate against the threat through the use of Proof Key for Code Exchange (PKCE, pronounced "pixy").

RFC 7636 - Proof Key for Code Exchange by OAuth Public Clients
Authorization Code Flowを使用するOAuth 2.0のPublic Clientは、認可コード横取り攻撃の影響を受けやすいので、コード交換のための証明鍵(PKCE、 "pixy"と発音)を使用して脅威を軽減します。

クライアント側でやること

code_verifier を生成する

The client first creates a code verifier, "code_verifier", for each OAuth 2.0 [RFC6749] Authorization Request, in the following manner:
code_verifier = high-entropy cryptographic random STRING using the unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" from Section 2.3 of [RFC3986], with a minimum length of 43 characters and a maximum length of 128 characters.

code_verifier = 予約されていない文字[A-Z] / [a-z] / [0-9] / " - " / "を使用した高エントロピー暗号化ランダムSTRING。 [RFC3986]のセクション2.3からの "/" _ "/"〜、最低長さ43文字、最大長さ128文字。  

code_challenge を生成する

The client then creates a code challenge derived from the code verifier by using one of the following transformations on the code verifier:

plain
      code_challenge = code_verifier
S256
      code_challenge = BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

If the client is capable of using "S256", it MUST use "S256", as "S256" is Mandatory To Implement (MTI) on the server.
Clients are permitted to use "plain" only if they cannot support "S256" for some technical reason and know via out-of-band configuration that the server supports "plain".
The plain transformation is for compatibility with existing deployments and for constrained environments that can't use the S256 transformation.

簡単にまとめると、生成したcode_verifierからcode_challengecode_challenge_method(S256)で計算する。
S256は、SHA-256でハッシュし、base64urlでエンコードする。 ※ code_challenge_methodは、クライアントがS256を使用できる場合はこれを使用する必要があり、技術的な理由でS256をサポートできず、サーバーがplainをサポートしていることが分かっている場合に、クライアントはplainを使用できます。

Authorization Requestでパラメータを追加する

4.1.1. Authorization Requestに、code_challengecode_challenge_methodを含める。

Access Token Requestでパラメータを追加する

4.1.3. Access Token Requestに、code_verifierを含める。

サーバー側でやること

code_challengeとcode_challenge_methodを保存する

When the server issues the authorization code in the authorization response, it MUST associate the "code_challenge" and "code_challenge_method" values with the authorization code so it can be verified later.

4.1.1. Authorization Requestで認可コードを発行するときに、code_challengecode_challenge_methodの値を、認可コードに紐づけて保存しておく。

code_challengeを比較検証する

Upon receipt of the request at the token endpoint, the server verifies it by calculating the code challenge from the received "code_verifier" and comparing it with the previously associated "code_challenge", after first transforming it according to the "code_challenge_method" method specified by the client.
If the "code_challenge_method" from Section 4.3 was "S256", the received "code_verifier" is hashed by SHA-256, base64url-encoded, and then compared to the "code_challenge", i.e.:

BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge

If the "code_challenge_method" from Section 4.3 was "plain", they are compared directly, i.e.:

code_verifier == code_challenge.

4.1.3. Access Token Requestでリクエストを受けると、サーバーは受け取ったcode_verifierからcode_challengeを計算し、認可コードに紐づけたcode_challengeと比較することによって検証します。

まとめ

PKCEは内容さえ理解してしまえば実装は難しくないので、プロバイダー側は是非実装しましょう。

第1正規形〜第3正規形の手順

非正規形と正規化のメリット

非正規形の特徴

簡単にいうと、正規化が行われていないテーブルを非正規形と呼びます。
例えば、購入履歴を管理するケースを想定して考えてみます。以下は非正規形のテーブル例です。
1回の注文を1行で管理しているため、注文の品が増えるとどんどん横に長くなってしまうという問題があります。

注文番号 注文日 ユーザー名 発送先 送料区分 商品名 単価 数量 商品名 単価 数量 商品名 単価 数量 合計金額
0001 2018/12/31 ユーザーA 住所A その他 商品A 100 1 商品B 50 5 商品D 200 1 550
0002 2019/01/01 ユーザーB 住所B 離島 商品A 100 1 100
0003 2019/01/04 ユーザーC 住所C その他 商品B 50 2 商品C 10 20 300

正規化のメリット

  • データの汎用性アップ
    データが整理されることで、他システムとの連携や移行などが行いやすくなります。
  • データの容量削減
    無駄な重複が削除されることにより、保存に必要なデータ領域の削減になります。
  • データの保守性アップ
    重複したデータがないようにするため、変更がある場合の修正が容易になります。

関数従属性

正規形はテーブルを関数従属性という基準に従って分類する作業である。
これは、もし1つの属性の値がわかれば、常に別の属性の値もわかる、という性質である。関係理論での表記法では、2つの属性の間に矢印を引く。
例えばA → Bは「AはBを決定する」とか「BはAに従属する」と読む。

例)
- 注文番号が分かれば注文日が分かる。
- 商品名が分かれば単価が分かる。

推移的関数従属性

関数従属性が推移的に行われていることを推移的関数従属性と呼びます。

例)
- Xが分かればYが分かる、Yが分かればZが分かる。ただしXが分かってもZは分からない。

第1正規形

横方向の繰り返しの整理をします。 RDBは縦にレコードを追加していく操作には適していますが、カラムは固定なので、横方向の繰り返しを縦に並べるように修正します。

注文番号 注文日 ユーザー名 発送先 送料区分 商品名 単価 数量 合計金額
0001 2018/12/31 ユーザーA 住所A その他 商品A 100 1 550
0001 2018/12/31 ユーザーA 住所A その他 商品B 50 5 550
0001 2018/12/31 ユーザーA 住所A その他 商品D 200 1 550
0002 2019/01/01 ユーザーB 住所B 離島 商品A 100 1 100
0003 2019/01/04 ユーザーC 住所C その他 商品B 50 2 300
0003 2019/01/04 ユーザーC 住所C その他 商品C 10 20 300

これで第1正規形の正規化は完了です。

第2正規形

従属関係にあるデータの分離を行います。

まずは、購入履歴と購入履歴明細に分離します。
<テーブル名: 購入履歴>

注文番号 注文日 ユーザー名 発送先 送料区分 合計金額
0001 2018/12/31 ユーザーA 住所A その他 550
0002 2019/01/01 ユーザーB 住所B 離島 100
0003 2019/01/04 ユーザーC 住所C その他 300

<テーブル名: 購入履歴明細>

注文番号 商品名 単価 数量
0001 商品A 100 1
0001 商品B 50 5
0001 商品D 200 1
0002 商品A 100 1
0003 商品B 50 2
0003 商品C 10 20

さらに、ユーザーデータと商品データに分離できます。
<テーブル名: 購入履歴>

注文番号 注文日 ユーザーID 合計金額
0001 2018/12/31 Y0001 550
0002 2019/01/01 Y0002 100
0003 2019/01/04 Y0003 300

<テーブル名: 購入履歴明細>

注文番号 商品ID 数量
0001 S0001 1
0001 S0002 5
0001 S0004 1
0002 S0001 1
0003 S0002 2
0003 S0003 20

<テーブル名: ユーザーデータ>

ユーザーID ユーザー名 発送先 送料区分
Y0001 ユーザーA 住所A その他
Y0002 ユーザーB 住所B 離島
Y0003 ユーザーC 住所C その他

<テーブル名: 商品データ>

商品ID 商品名 単価
S0001 商品A 100
S0002 商品B 50
S0003 商品C 10
S0004 商品D 200

これで第2正規形の正規化は完了です。

第3正規形

推移的関数従属と呼ばれる従属関係の分離を行います。

ユーザーデータの送料区分を分離します。 <テーブル名: ユーザーデータ>

ユーザーID ユーザー名 発送先 送料区分コード
Y0001 ユーザーA 住所A 01
Y0002 ユーザーB 住所B 02
Y0003 ユーザーC 住所C 01

<テーブル名: 送料区分データ>

送料区分コード 送料区分名称 送料
01 その他 500
02 離島 1000

これで第3正規形の正規化は完了です。

ボイスコッド正規形

主キー以外のカラムが全て主キーに完全関数従属であり、それ以外の従属関係があればテーブルを分離することがボイスコッド正規形の正規化になります。

ユーザーデータにメールアドレスが含まれるとして、登録時に登録済みメールアドレスの再登録をエラーにする処理などを行っていれば、ユーザー名とメールアドレスは1:1の関係にあると言えます。 その場合、ボイスコッド正規形の正規化では、テーブルを2つに分離することになります。 <テーブル名: ユーザーデータ(分離前)>

ユーザーID ユーザー名 発送先 メールアドレス 送料区分コード
Y0001 ユーザーA 住所A A@aaa.jp 01
Y0002 ユーザーB 住所B B@bbb.jp 02
Y0003 ユーザーC 住所C C@ccc.jp 01

<テーブル名: ユーザーデータ(分離後)>

ユーザーID ユーザー名 発送先 送料区分コード
Y0001 ユーザーA 住所A 01
Y0002 ユーザーB 住所B 02
Y0003 ユーザーC 住所C 01

<テーブル名: メールアドレスデータ(分離後)>

ユーザーID メールアドレス
Y0001 A@aaa.jp
Y0002 B@bbb.jp
Y0003 C@ccc.jp

これでボイスコッド正規形の正規化は完了です。

まとめ

正規形で重要なのは第3正規形までで、まれにボイスコッド正規形を利用する機会があります。
本稿で述べたような、横方向に伸ばすようなデータ、重複するようなデータを最適化することが正規化の作業だと考えます。

OAuth2.0について改めて調べてみた

OAuth2.0とは、Webアプリケーション等に特定の認可フローを提供しながら、クライアント開発者の簡潔さに焦点を合わせている業界標準の認可プロトコルです。

1 認可コードフロー(Authorization Code Flow)

4.1. Authorization Code Grantで定義されているフロー。
認可サーバーがクライアントとリソースオーナーの仲介となって発行する。
リソースオーナーへ直接認可を要求する代わりに、クライアントはリソースオーナーを認可サーバーへリダイレクトさせ、リソースオーナーがリダイレクトして戻ってきた際に認可コードを取得する。
リソースオーナーを認可コードとともにリダイレクト経由でクライアントに戻す前に、認可サーバーはリソースオーナーを認証し認可を得る。
これによりリソースオーナーは認可サーバーによってのみ認証され, リソースオーナーのクレデンシャルはクライアントへ共有されない。
認可コードはクライアントを認証する機会を提供したり、リソースオーナーのユーザーエージェントを経由せずアクセストークンをクライアントに直接送信できるなど、いくつかの点で重要なセキュリティ的なメリットを提供する。
リソースオーナーのユーザーエージェントを経由してデータを送信した場合、そのデータはリソースオーナーを含む第三者に露出する可能性がある。
f:id:acquapazza:20181219030546p:plain

1.1 認可エンドポイントへのリクエス

https://tools.ietf.org/html/rfc6749#section-4.1.1

GET {認可エンドポイント}
    ?response_type=code // 必須 codeを指定
    &client_id={クライアントID} // 必須 
    &redirect_uri={リダイレクトURI} // 任意
    &scope={スコープ群} // 任意
    &state={任意文字列} // 推奨
    HTTP/1.1
Host: {認可サーバー}
続きを読む

gormのupdateのハマりどころ

gormでupdateするときのハマりどころをまとめました。

gormのUpdatesについて

// 下記のレコードが登録済み
//   id | name | activated
// -----+------+----------
//    1 |  Bob |      true

id = 1のactivatedをfalseに更新する方法をいくつか挙げます。

structを渡す場合

beforeUser := User{
    ID: 1,
}

// activatedをfalseに更新する
db.Model(&beforeUser).Updates(User{Activated: false})

afterUser := User{}
// 1レコード目を取得する
db.First(&afterUser)

// 更新できたか確認
fmt.Printf("ID: %d, Activated: %v\n", afterUser.ID, afterUser.Activated)

// ID: 1, Activated: true

更新できていませんでした。

mapを渡す場合

beforeUser := User{
    ID: 1,
}

// activatedをfalseに更新する
db.Model(&user).Updates(map[string]interface{}{"activated": false})

afterUser := User{}
// 1レコード目を取得する
db.First(&afterUser)

// 更新できたか確認
fmt.Printf("ID: %d, Activated: %v\n", afterUser.ID, afterUser.Activated)

// ID: 1, Activated: false

正しく更新できました。

なぜこのようなことが起こるのか

gorm/scope.go

func (scope *Scope) Fields() []*Field {
    if scope.fields == nil {
        var (
            fields             []*Field
            indirectScopeValue = scope.IndirectValue()
            isStruct           = indirectScopeValue.Kind() == reflect.Struct
        )

        for _, structField := range scope.GetModelStruct().StructFields {
            if isStruct {
                fieldValue := indirectScopeValue
                for _, name := range structField.Names {
                    fieldValue = reflect.Indirect(fieldValue).FieldByName(name)
                }
                fields = append(fields, &Field{StructField: structField, Field: fieldValue, IsBlank: isBlank(fieldValue)})
            } else {
                fields = append(fields, &Field{StructField: structField, IsBlank: true})
            }
        }
        scope.fields = &fields
    }

    return *scope.fields
}

func isBlank(value reflect.Value) bool {
    return reflect.DeepEqual(value.Interface(), reflect.Zero(value.Type()).Interface())
}

func convertInterfaceToMap(values interface{}) map[string]interface{} {
    var attrs = map[string]interface{}{}

    switch value := values.(type) {
    case map[string]interface{}:
        return value
    case []interface{}:
        for _, v := range value {
            for key, value := range convertInterfaceToMap(v) {
                attrs[key] = value
            }
        }
    case interface{}:
        reflectValue := reflect.ValueOf(values)

        switch reflectValue.Kind() {
        case reflect.Map:
            for _, key := range reflectValue.MapKeys() {
                attrs[ToDBName(key.Interface().(string))] = reflectValue.MapIndex(key).Interface()
            }
        default:
            for _, field := range (&Scope{Value: values}).Fields() {
                if !field.IsBlank {
                    attrs[field.DBName] = field.Field.Interface()
                }
            }
        }
    }
    return attrs
}

大事なのは上記の1メソッドと2関数。
Fields()はstructの場合、各フィールドに値が入っているかをisBlankという関数を用いて判定している。
isBlankは各型ごとのゼロ値かどうかを判定しているが、falseに更新したい場合はisBlankがtrueになってしまう。
convertInterfaceToMapで最終的に更新するフィールドと値をセットしているが、structの場合は case interface{}: に該当し、Mapではない為 default: が処理される。
ここで IsBlank = true だと更新対象外となってします。

一方 map[string]interface{} で更新する場合は、isBlankは関係なく case map[string]interface{}: が処理される為、もれなく更新されます。

まとめ

当たり前のように更新されると思いきや、実装方法によっては更新されない場合があるので、どういう場合に更新されるのか、ライブラリの実装をちゃんと読んで理解することは大切ですね。

go time.TimeのAddDateの落とし穴

golangのtime.TimeのAddDateが意図しない日時を返してきたので、備忘録として書きます。

AddDateの一般的な使い方

package main

import (
    "fmt"
    "time"
)

func main() {
    // 現在時刻を取得
    now := time.Now()
    fmt.Printf("%v (now)\n", now)
    // +1 year
    fmt.Printf("%v (+1 year)\n", now.AddDate(1, 0, 0))
    // +1 month
    fmt.Printf("%v (+1 month)\n", now.AddDate(0, 1, 0))
    // +1 day
    fmt.Printf("%v (+1 day)\n", now.AddDate(0, 0, 1))
}

// 2009-11-10 23:00:00 +0000 UTC m=+0.000000001 (now)
// 2010-11-10 23:00:00 +0000 UTC (+1 year)
// 2009-12-10 23:00:00 +0000 UTC (+1 month)
// 2009-11-11 23:00:00 +0000 UTC (+1 day)

The Go Playground

AddDateは、
第一引数にyearの増減値
第二引数にmonthの増減値
第三引数にdayの増減値
を設定する。

注意が必要な使い方

増減して取得する月の日数が、元の月の日数より少ないときに、月末を取得する場合

例) 元: 8/31、取得したい日: 9/30

package main

import (
    "fmt"
    "time"
)

func main() {
    location, err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        return
    }
    date := time.Date(2018, 8, 31, 0, 0, 0, 0, location)
    fmt.Printf("%v (date)\n", date)
    // +1 month
    fmt.Printf("%v (+1 month)\n", date.AddDate(0, 1, 0))
}

// 2018-08-31 00:00:00 +0900 JST (date)
// 2018-10-01 00:00:00 +0900 JST (+1 month)

The Go Playground
9/30が取得できるかと思いきや、10/1が取得できてしまいます。

2月末を取得する場合

package main

import (
    "fmt"
    "time"
)

func main() {
    location, err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        return
    }
    date := time.Date(2018, 1, 31, 0, 0, 0, 0, location)
    fmt.Printf("%v (date)\n", date)
    // +1 month
    fmt.Printf("%v (+1 month)\n", date.AddDate(0, 1, 0))
}

// 2018-01-31 00:00:00 +0900 JST (date)
// 2018-03-03 00:00:00 +0900 JST (+1 month)

The Go Playground
2/28が取得できるかと思いきや、3/3が取得できてしまいます。
閏年の場合は3/2が取得できます。

Appendix

うるう秒

前回挿入されたのは2017/1/1 09:00の直前

package main

import (
    "fmt"
    "time"
)

func main() {
    location, err := time.LoadLocation("Asia/Tokyo")
    if err != nil {
        return
    }
    date := time.Date(2017, 1, 1, 8, 59, 58, 0, location)
 
    for {
        date = date.Add(time. Millisecond * 100)
        fmt.Printf("%v\n", date.Add(time. Millisecond * 100))
        if date.After(time.Date(2017, 1, 1, 9, 0, 0, 0, location)) {
            break
        }
    }
}

// 2017-01-01 08:59:58.2 +0900 JST
// 2017-01-01 08:59:58.3 +0900 JST
// 2017-01-01 08:59:58.4 +0900 JST
// 2017-01-01 08:59:58.5 +0900 JST
// 2017-01-01 08:59:58.6 +0900 JST
// 2017-01-01 08:59:58.7 +0900 JST
// 2017-01-01 08:59:58.8 +0900 JST
// 2017-01-01 08:59:58.9 +0900 JST
// 2017-01-01 08:59:59 +0900 JST
// 2017-01-01 08:59:59.1 +0900 JST
// 2017-01-01 08:59:59.2 +0900 JST
// 2017-01-01 08:59:59.3 +0900 JST
// 2017-01-01 08:59:59.4 +0900 JST
// 2017-01-01 08:59:59.5 +0900 JST
// 2017-01-01 08:59:59.6 +0900 JST
// 2017-01-01 08:59:59.7 +0900 JST
// 2017-01-01 08:59:59.8 +0900 JST
// 2017-01-01 08:59:59.9 +0900 JST
// 2017-01-01 09:00:00 +0900 JST
// 2017-01-01 09:00:00.1 +0900 JST
// 2017-01-01 09:00:00.2 +0900 JST

The Go Playground
8:59:59が2回きています。