CakePHP3 に関する投稿を表示しています

authorization と authentication の話。そして Laravel の Gate の紹介をちらっと。

要するに、認証と認可は別物だよって話。

 

ざっくりとまとめると。

認証 Authentication
あなたは誰ですか
私は X というユーザです、パスワードはこれです
確認しました X で間違いないです

 

認可 Authorization
X です、管理画面を開きたいです
あなたは管理画面への"鍵"をもっていません、拒否します

 

もうちょい例。
世帯主じゃなくても、家の鍵を持っていれば家に入れる。=認可
世帯主かどうかは公的文書(住民票とか)で確認する。=認証

 

で、じゃあ Web ではどうやんねん、というと、
(例によってよく使うものだけ紹介)

CakePHP ならこのあたりにまとまってる。

認証 - 3.6

 

Laravel だと分かれてる。使う仕組みが違うので別項目になってるって。

Authentication - Laravel - The PHP Framework For Web Artisans
Authorization - Laravel - The PHP Framework For Web Artisans


Laravel の Gate の説明をちゃんと見てみたの初めてだけど Gate かっちょいいな、権限管理とか、できるできないみたいな話がわりかし簡単にかけそう。

できるできないはこういう風に AuthServiceProvider->boot に書いたり。

Gate::define('update-post' function ($user $post) {
    return $user->id == $post->user_id;
});

こういう形で クラス名 @ メソッド名 として外だししたり。

Gate::define('update-post' 'PostPolicy@update');
class PostPolicy
{
    public function update(User $user Post $post)
    {
        return $user->id == $post->user_id;
    }
}

でもって、定義した Gate は、コントローラーだったりモデルだったりで、こうやって使えるらしい。

if (Gate::forUser($user)->allows('update-post' $post)) {
    // The user can update the post...
}

あとは、ポリシーを定義することができて(↑にある PostPolicy@update という書き方で使えるものとは異なる)
Eloquant に対して view/create/update/delete がそれぞれ権限ありますか?というのをお手軽に内部でつないでくれるらしい。

たぶん実際のユースケース的にはいろいろ分割しやすいこの書き方がいいんじゃなかろうか。

AuthServiceProvider はこういう感じで

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        Post::class => PostPolicy::class,
    ];
...

ポリシークラスのほうはこんな感じ。

namespace App\Policies;

use App\User;
use App\Post;

class PostPolicy
{
    public function update(User $user Post $post)
    {
        return $user->id === $post->user_id;
    }
}

さっきの update とまったく同じなんだけど、こうすることで AuthServiceProvider が勝手に Post を監視してくれて PostPolicy に書かれている通り update 時のチェックを実行して true なら update ができる、というようなことをやってくれるんだそうだ。

便利~~~~。

 

例えば wordpress 的なものをイメージして、簡単にやってみると。

  • 投稿
    • 管理者でログインしていれば投稿の作成、編集、閲覧、公開設定にすることが可能
    • 編集者でログインしていれば投稿の作成、編集、閲覧、が可能。公開設定にはできない
    • ログインしていなければ投稿の閲覧が可能
  • コメント
    • 管理者でログインしていればコメントの作成、削除、公開されたコメントの閲覧、管理者向けコメントの閲覧が可能
    • それ以外は作成と、公開されたコメントの閲覧のみ可能

みたいなのも Gate をつかったらぱっぱっぱ~~~ってかけそうだって思った。
投稿ポリシーとコメントポリシーを、それぞれ書いてある通りに定義すればいいだけだし、仕様を形にするのが簡単そうだ。

んん~~~、使いどころがあれば使いたいなあ。

CakePHP3 で論理削除をする

CakePHP3 ではデフォルトでは論理削除の機能は実装されていない。調べると1番上にいい感じに利用できるプラグインがあるので、これを利用するとよい。

PGBI/cakephp3-soft-delete

とりあえず PGBI/cakephp3-soft-delete を使ってみる

composer でパッケージを追加して。

$ composer require pgbi/cakephp3-soft-delete "~1.0"

プラグインの読み込みを追加して。

// config/bootstrap.php
Plugin::load('SoftDelete');

テーブルで SoftDeleteTrait を読み込んで。

// src/Model/Table/UserTable.php

...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;
    ...

該当のテーブルに deleted というカラムを追加して。

// config/Migrations/20171114120000_CreateUsers.php
...

$table->addColumn('deleted', 'datetime', [
    'default' => null,
    'null' => true,
]);

...

Enjoy!

// コンソール機能を使って動作確認をしてみる
//==============================
$ bin/cake console


// Table の読み込み
//==============================
>>> use Cake\ORM\TableRegistry;
>>> $Users = TableRegistry::get('Users');
=> App\Model\Table\UsersTable {#214
     +"registryAlias": "Users",
     +"table": "users",
     +"alias": "Users",
     +"entityClass": "App\Model\Entity\User",
     +"associations": [],
     +"behaviors": [
       "Timestamp",
     ],
     +"defaultConnection": "default",
     +"connectionName": "default",
   }


// Entity の作成
//==============================
>>> $entity = $Users->newEntity();
=> App\Model\Entity\User {#216
     +"[new]": true,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データの保存
//==============================
>>> $Users->save($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#218
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// データ保存の確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#295
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#286
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#285
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// SoftDeleteTrait を読み込むと delete の挙動が SoftDelete に変わる
//==============================
>>> $Users->delete($entity);
=> true


// SoftDelete されたデータが検索できないことを確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'


// 'withDeleted' をつけることによって SoftDelete されたデータを検索できる
//==============================
>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> App\Model\Entity\User {#521
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#512
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#511
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": Cake\I18n\FrozenTime {#522
       +"time": "2017-11-14T12:42:48+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore によって SoftDelete されたデータを元に戻せる
//==============================
>>> $Users->restore($entity);
=> App\Model\Entity\User {#216
     +"created": Cake\I18n\Time {#232
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\Time {#229
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"id": 2,
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// restore の結果について確認
//==============================
>>> $Users->get(2);
=> App\Model\Entity\User {#548
     +"id": 2,
     +"created": Cake\I18n\FrozenTime {#539
       +"time": "2017-11-14T12:41:19+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"modified": Cake\I18n\FrozenTime {#538
       +"time": "2017-11-14T12:44:30+09:00",
       +"timezone": "Asia/Tokyo",
       +"fixedNowTime": false,
     },
     +"deleted": null,
     +"[new]": false,
     +"[accessible]": [],
     +"[dirty]": [],
     +"[original]": [],
     +"[virtual]": [],
     +"[errors]": [],
     +"[invalid]": [],
     +"[repository]": "Users",
   }


// hardDelete をすることによって、復元できない削除をする
//==============================
>>> $Users->hardDelete($entity);
=> true


// hardDelete の結果について確認
//==============================
>>> $Users->get(2);
Cake\Datasource\Exception\RecordNotFoundException with message 'Record not found in table "users"'

>>> $Users->find('all', ['withDeleted'])->where(['id' => 2])->first();
=> null

かゆいところに手を伸ばす

デフォルトでは deleted というカラムだが、テーブルにプロパティを追加することで違うカラム名にもできる。

// UserTable.php
...

use SoftDelete\Model\Table\SoftDeleteTrait;

class UsersTable extends Table
{
    use SoftDeleteTrait;

    protected $softDeleteField = 'kokoga_deleted_no_field_desu';

    ..

とはいえ CakePHP3 におけるデフォルトのタイムスタンプのフィールドは created と modified という名前になっているので、そこは deleted という名前で合わせにいくのがわかりやすさも高くて良いと思う。

Bitbucket Pipelines で MySQL コンテナを使う時に早すぎるとエラーになる

この記事は公開されてから1年以上経過しており、情報が古い可能性があります。

Bitbucket Pipelines を使ってて、全く設定を変えていないのに MySQL に繋がったり繋がらなくなったりしていたので、どういうこっちゃ、というメモ。

結論から行くと 3 〜 5 秒くらい待ってあげればよい。

MySQL に繋がらない

エラーとしてはこんな感じ。ちなみにこの問題と直接関係はないが CakePHP 3.5 である。

+ bin/cake migrations migrate
using migration paths 
 - /opt/atlassian/pipelines/agent/build/config/Migrations
using seed paths 
 - /opt/atlassian/pipelines/agent/build/config/Seeds
Exception: There was a problem connecting to the database: SQLSTATE[HY000] [2002] Connection refused in [/opt/atlassian/pipelines/agent/build/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/MysqlAdapter.php, line 115]
2017-10-16 10:57:41 Error: [InvalidArgumentException] There was a problem connecting to the database: SQLSTATE[HY000] [2002] Connection refused in /opt/atlassian/pipelines/agent/build/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/MysqlAdapter.php on line 115
Stack Trace:
#0 /opt/atlassian/pipelines/agent/build/vendor/robmorgan/phinx/src/Phinx/Db/Adapter/PdoAdapter.php(238): Phinx\Db\Adapter\MysqlAdapter->connect()
#1 /opt/atlassian/pipelines/agent/build/vendor/cakephp/migrations/src/CakeAdapter.php(57): Phinx\Db\Adapter\PdoAdapter->getConnection()
#2 /opt/atlassian/pipelines/agent/build/vendor/cakephp/migrations/src/Command/CommandTrait.php(78): Migrations\CakeAdapter->__construct(Object(Phinx\Db\Adapter\MysqlAdapter), Object(Cake\Database\Connection))
#3 /opt/atlassian/pipelines/agent/build/vendor/robmorgan/phinx/src/Phinx/Console/Command/Migrate.php(72): Migrations\Command\Migrate->bootstrap(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#4 /opt/atlassian/pipelines/agent/build/vendor/cakephp/migrations/src/Command/CommandTrait.php(35): Phinx\Console\Command\Migrate->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#5 /opt/atlassian/pipelines/agent/build/vendor/cakephp/migrations/src/Command/Migrate.php(65): Migrations\Command\Migrate->parentExecute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#6 /opt/atlassian/pipelines/agent/build/vendor/symfony/console/Command/Command.php(262): Migrations\Command\Migrate->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#7 /opt/atlassian/pipelines/agent/build/vendor/symfony/console/Application.php(888): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#8 /opt/atlassian/pipelines/agent/build/vendor/symfony/console/Application.php(224): Symfony\Component\Console\Application->doRunCommand(Object(Migrations\Command\Migrate), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#9 /opt/atlassian/pipelines/agent/build/vendor/symfony/console/Application.php(125): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#10 /opt/atlassian/pipelines/agent/build/vendor/cakephp/migrations/src/Shell/MigrationsShell.php(101): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
#11 /opt/atlassian/pipelines/agent/build/vendor/cakephp/cakephp/src/Console/Shell.php(508): Migrations\Shell\MigrationsShell->main('migrations', 'migrate')
#12 /opt/atlassian/pipelines/agent/build/vendor/cakephp/migrations/src/Shell/MigrationsShell.php(156): Cake\Console\Shell->runCommand(Array, true, Array)
#13 /opt/atlassian/pipelines/agent/build/vendor/cakephp/cakephp/src/Console/CommandRunner.php(141): Migrations\Shell\MigrationsShell->runCommand(Array, true)
#14 /opt/atlassian/pipelines/agent/build/bin/cake.php(12): Cake\Console\CommandRunner->run(Array)
#15 {main}
script:
	- composer install --no-interaction
	- chmod +x bin/cake
	- chmod -R 777 tmp/ logs/
	- bin/cake migrations migrate
	- composer test

bitbucket-pipelines.yml に記載した実行スクリプトに関してはこれだけなのだが、マイグレーションのところで、トピックブランチでは通って、マスターブランチでは通らず、なんてことがぼちぼち起きた。

Bitbucket Pipelines 上で MySQL のログが見えるので、これを確認すると、通らなかったときには、通ったときのログが途中できれているようなものだった。

Initializing database
...(中略)...

2017-10-16T01:57:41.831583Z 0 [Note] End of list of non-natively partitioned tables

成功した時はこんな感じの最終行になる。

2017-10-16T01:51:55.506134Z 0 [Note] InnoDB: Starting shutdown...
2017-10-16T01:51:55.606342Z 0 [Note] InnoDB: Dumping buffer pool(s) to /var/lib/mysql/ib_buffer_pool
2017-10-16T01:51:55.606688Z 0 [Note] InnoDB: Buffer pool(s) dump completed at 171016  1:51:55

つまり MySQL コンテナが何かしらの初期化処理をしていて、にも関わらず繋ごうとしたから接続できないエラー?そうして Bitbucket Pipelines がエラーを認識して停止。ふむ、納得出来る気がする。

アプリケーション側で接続時のタイムアウト設定

とりあえず思いつくスマートな方法はこれ。

このプロジェクトは CakePHP3 を使っているが CakePHP3 には接続時のタイムアウト設定な項目はないらしい。かなしい。とはいえ何かあるだろうとソースを読み進めると 最終的に PDO を使っているらしいことがわかる。ので PDO の設定方法を調べてみると PDO::ATTR_TIMEOUT というオプションを設定するとよいらしい。

php - Setting a connect timeout with PDO - Stack Overflow

このオプションを CakePHP3 で設定するには、コンフィグ内の flags にいれると出来そうなソースをしている。

cakephp/PDODriverTrait.php at master · cakephp/cakephp

$connection = new PDO(
	$dsn,
	$config['username'],
	$config['password'],
	$config['flags']
);

しかし入れても状況は改善されなかったので、違うらしい。。これはまたそのうち調べよう。。。

bitbucket-pipelines.yml に sleep を入れる

というわけでこっちの方法。yml (の抜粋)はこんな感じ。

script:
	- composer install --no-interaction
	- chmod +x bin/cake
	- chmod -R 777 tmp/ logs/
	- sleep 5
	- bin/cake migrations migrate
	- composer test

これで安定的に動いてくれているので、このままでいいか。

MySQL コンテナの初期化処理

そもそもとして MySQL コンテナの初期化処理って何をしているんだろうか。Dockerfile はここ。どうやら docker-entrypoint.sh が何かしていそうだ。

mysql/Dockerfile at master · docker-library/mysql
mysql/docker-entrypoint.sh at master · docker-library/mysql

すごいざっくり読むとこんなことをしているっぽい。

  • Docker イメージが提供するのは mysqld の準備~起動、 docker-entrypoint.sh の起動まで
  • docker-entrypoint.sh によって初回起動によるデータファイル構築。設定したデータベース、ユーザの準備。ちなみに途中で mysqld の再起動をしている。

なるほど理解。
やっぱりコンテナが立ち上がったと同時に初期化処理が走って、それが終わるまでは接続が出来ないようだ。

ものすごいバッドプラクティスを感じている sleep 5 なのでなんとかしたいものの、どうこうするのがいいのかイマイチわからないので、知見あるひと教えてほしス