Laravel 提供兩種主要的 authorizing action:gate 跟 policy

可以把 gates 跟 policies 想像成 routes 跟 controllers。

Gates are most applicable to actions which are not related to any model or resource, such as viewing an administrator dashboard. In contrast, policies should be used when you wish to authorize an action for a particular model or resource.

通常 gates 跟 policies 會混合使用。

Gates

Gates are simply closures that determine if a user is authorized to perform a given action.

gates 通常定義在 App\Providers\AuthServiceProvider::boot(),用 Gate facade 來定義:

1
2
3
4
5
6
7
8
public function boot()
{
$this->registerPolicies();

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

也可以用 class callback 形式:

1
Gate::define('update-post', [PostPolicy::class, 'update']);

定義 gate 後,要在需要擋權限的地方(例如 controller action)使用 Gate::allows()Gate::denies()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PostController extends Controller
{
/**
* Update the given post.
*/
public function update(Request $request, Post $post)
{
if (! Gate::allows('update-post', $post)) {
abort(403);
}

// Update the post...
}
}

不用自己丟目前登入的 user instance 進去,laravel 會 handle 這件事。

如果不是要看現在登入的 user,而是要 authorize 另外的 user,可以使用 forUser()

1
2
3
4
5
6
7
if (Gate::forUser($user)->allows('update-post', $post)) {
// The user can update the post...
}

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

可以用 any()none() 來同時 authorize 多個 action:

1
2
3
4
5
6
7
if (Gate::any(['update-post', 'delete-post'], $post)) {
// The user can update or delete the post...
}

if (Gate::none(['update-post', 'delete-post'], $post)) {
// The user can't update or delete the post...
}

如果想要在 authorize 失敗的時候自動丟 Illuminate\Auth\Access\AuthorizationException

1
Gate::authorize('update-post', $post);

Illuminate\Auth\Access\AuthorizationException 會自動被轉成 403 response by Laravel’s exception handler。

可以 support 複雜的 context:https://laravel.com/docs/8.x/authorization#gates-supplying-additional-context

Gate define 的時候也可以 return Illuminate\Auth\Access\Response 來帶更多資訊。Ref

如果想要讓某個 user 可以 grant all ability,可以用 before() Ref

1
2
3
4
5
Gate::before(function ($user, $ability) {
if ($user->isAdministrator()) {
return true;
}
});

If the before closure returns a non-null result that result will be considered the result of the authorization check.

也可以用 after 來定義在所有 authorization check 執行後執行的 closure。

Policies

Policies are classes that organize authorization logic around a particular model or resource.

可以用 make:policy 來產生 policy class,產生的 policy 會放在 app/Policies/

如果想要有跟某個 model 關聯的 example policy:

1
$ php artisan make:policy PostPolicy --model=Post

Register policy

有 policy class 後,要 register 它。

Registering policies is how we can inform Laravel which policy to use when authorizing actions against a given model type.

App\Providers\AuthServiceProvider 裡有個 policies property,它的 key 是 model class,value 是對應的 policy class name。

Registering a policy will instruct Laravel which policy to utilize when authorizing actions against a given Eloquent model:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AuthServiceProvider extends ServiceProvider
{
/**
* The policy mappings for the application.
*
* @var array
*/
protected $policies = [
Post::class => PostPolicy::class,
];

/**
* Register any application authentication / authorization services.
*
* @return void
*/
public function boot()
{
$this->registerPolicies();

//
}
}

Write Policy

大致長這樣,可以寫任意 function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class PostPolicy
{
/**
* Determine if the given post can be updated by the user.
*
* @param \App\Models\User $user
* @param \App\Models\Post $post
* @return bool
*/
public function update(User $user, Post $post)
{
return $user->id === $post->user_id;
}
}

如果用 --model 生,會生出 CRUD 有關的 function。

使用 Policy

透過 User Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PostController extends Controller
{
/**
* Update the given post.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Post $post
* @return \Illuminate\Http\Response
*/
public function update(Request $request, Post $post)
{
if ($request->user()->cannot('update', $post)) {
abort(403);
}

// Update the post...
}
}

沒有 model instance 的 policy:

1
2
3
4
5
6
7
8
public function store(Request $request)
{
if ($request->user()->cannot('create', Post::class)) {
abort(403);
}

// Create the post...
}

透過 Controller Helpers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PostController extends Controller
{
/**
* Update the given blog post.
*
* @param \Illuminate\Http\Request $request
* @param \App\Models\Post $post
* @return \Illuminate\Http\Response
*
* @throws \Illuminate\Auth\Access\AuthorizationException
*/
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);

// The current user can update the blog post...
}
}

如果是 resource controller,可以這樣:

1
2
3
4
5
6
7
8
9
10
11
12
class PostController extends Controller
{
/**
* Create the controller instance.
*
* @return void
*/
public function __construct()
{
$this->authorizeResource(Post::class, 'post');
}
}

要注意 CRUD 的 function 的 signature 要符合 laravel 需要的 type hint 才能用

透過 middleware

1
2
3
Route::put('/post/{post}', function (Post $post) {
// The current user may update the post...
})->middleware('can:update,post');

MySQL UUID Smackdown: UUID vs. INT for Primary Key

可以用 BINARY 存 UUID,然後用以下 function 做 human-readable format 的轉換:

  • UUID_TO_BIN
  • BIN_TO_UUID
  • IS_UUID

這些 function 要 MySQL 8.0 後才有。

Example

把 UUID 當 PK 的 example

create table

1
2
3
4
CREATE TABLE customers (
id BINARY(16) PRIMARY KEY,
name VARCHAR(255)
);

insertion

1
2
3
4
INSERT INTO customers(id, name)
VALUES(UUID_TO_BIN(UUID()),'John Doe'),
(UUID_TO_BIN(UUID()),'Will Smith'),
(UUID_TO_BIN(UUID()),'Mary Jane');

query

1
2
3
4
5
SELECT 
BIN_TO_UUID(id) id,
name
FROM
customers;

以 build & deploy blog 練習使用 CodePipeline 相關 service。

  1. 在 CodeCommit 開 repository,把 blog source 放上去。CodeCommit 是類似 Github 的 AWS service。
  2. CodePipeline 的 Source stage 設定 source 為 CodeCommit。
  3. Build stage 使用 CodeBuild,image 用 AWS 的 Ubuntu image。
  4. 在 source code 裡加入 buildspec.yml,讓 CodeBuild 知道要跑什麼,內容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
version: 0.2

phases:
install:
on-failure: ABORT
commands:
- ./blog init
build:
on-failure: ABORT
commands:
- ./blog g

artifacts:
files:
- '**/*'
base-directory: public
name: blog-$(date +%Y-%m-%d)
  1. deploy stage 設定 deploy 到 S3 的 static web hosting bucket。

pipeline 會在 push code 後自動開始跑,一路 build 出 static html、deploy 到 S3。source stage 跟 build stage 的 artifact 會存到 S3。

Ref

這章在講設計 API 的 best practice。

Designing for Real-Life Use Cases

設計 API 時要以特定的、實際的 use case 進行決策,不要空想。

千萬不要公開內部基礎架構,把焦點放在與外部開發者(或 API 使用者)與 API 的互動體驗上。

藉由選擇特定的工作流程或 use case,你可以把重點放在一項設計上,並測試它是否可以幫助你的使用者。

在 brainstorming 階段去想「如果怎樣的話…」是有幫助的,可是到了設計階段,太多的「如果怎樣的話」的想像,反而會讓設計失焦,所以針對特定的 use case 是比較好的。

Designing for a Great Developer Experience

提供開發者好的開發體驗。

讓 API 能更快、更容易上手

文件可以幫開發者上手,有 tutorial 跟 getting started guide 也很好。

也可以提供線上互動式文件、線上的 sandbox 來讓 developer 可以實際測試。

提供 SDK 也可以幫助 developer 使用 API。

最後是應該要讓 developer 不需要登入或註冊就能使用 API。如果一定得要註冊,要盡可能減少註冊需要的資料。如果 API 使用 OAuth 保護,那麼應該要能讓 developer 在 UI 中產生 token 以使用 API。

維持一致性

像是 entry nmae、request 參數、response 等等,應該要維持一致性,讓 developer 即使不看文件也能猜到部份的 API。

在漸進修改的過程中,盡量與既有的設計模式保持一致,對使用者來說是最好的做法。

不要讓同樣的東西使用不同的名稱。

一致性很重要的原因是它可以減少試著了解你的 API 的 developer 的認知負擔(cognitive load)

Make Troubleshooting Easy

藉由回傳有意義的 error 跟 building tool 做到這點。

設計 API 時應該有系統的組織跟分類錯誤以及他們的回傳方式來方便 developer 排除問題。

有意義的 error 容易了解、明確而且可以讓人採取行動。它們可以協助 developer 了解問題並處理它。提供這些 error 的 detail 可以帶來較佳的使用者體驗。

machine-readable 的 error code 字串可以讓 developer 以程式處理錯誤。

除了 machine-readable 的 error code 之外,也可以加入 human-readable 的敘述,來讓 developer 更了解發生了什麼問題。

error 要講出具體錯誤的原因,像是「token 因為被撤銷而造成驗證失敗」,用 token_revoked 比 invalid_auth 好。

幫 error 分類

將 API request 過程(從 request 開始,到 architecture 的各種 service 邊界)的各種 high-level error 分門別類,例如:

按照程式碼路徑將 error 分類後,要考慮對這些 error 而言,採取哪個 level 的 communication 是有意義的。一種方式是在 response payload 中放入 HTTP status code 跟 header,以及 machine-readable 的 code 或更詳細的 human-readable 的錯誤訊息。

大部分情況下,要盡量具體的說明來讓 developer 採取正確的後續動作。但有些時候,尤其跟安全有關的時候,可能要回傳比較籠統的資訊,不然原始訊息可能會透漏如資料庫 connection 資訊等會引發安全問題的資訊。

在程式結構上,可以用一套相同的 library 檢查 request 並將 error format 成相同的格式來 response。

將 error 文件化,像是寫在 API 文件中。

與 HTTP API 錯誤與問題相關可以參考 RFC 7807

Build tooling

log HTTP status、error、error 的頻率以及其他 request metadata,好方便在內部或外部進行 debug 或處理問題。

建立 dashboard 來協助 developer 分析 API request 的 metadata,例如可以統計最常用的 API entry、找出沒被用過的 API 參數、分類常見錯誤等等。

log 跟 dashboard 都有很多現成的工具。

Make Your API Extensible

要擬定 API 的發展策略,讓 API 是可擴展的(extensible)。

API 應該提供可開啟新工作流程的基本元素,而非只是對映你的 app 的工作流程。API 的建立方式決定了 API 的使用者可用他來做什麼事情。如果你提供太低階的操作,可能會讓整合者負擔太多工作。如果你提供太高階的操作,可能會讓大多數的整合只是對應你自己的 app 所作的工作而已。為了實踐創新,你必須找到適當的平衡點,讓使用者能夠啟動不屬於你的 app 或 API 本身的工作流程。

在前後端分離的架構下,要區分內部使用跟對外公開的 API。兩者對於要提供什麼樣的 API 會有不同的考量。

extensible 的其中一個部份是確保 top partner 有提供 feedback 的機會。要設法 release 某些功能給 top partner 用用看,讓他們給予 feedback。

在想要用版本管理 API 時,如果早期就加入版本管理系統會比較容易,越晚加入越難實作。版本管理系統的好處在於它可以讓你用新版本進行 breaking changes,同時又能維持就版本的回溯相容性(backward compatibility)。breaking changes 就是會讓之前使用你的 API 可以正常運作的 app 無法繼續運作的 changes。

不過,維護版本是需要成本的。如果很多年沒有能力支援舊版本,或者認為 API 不太需要變動,那麼可以不用版本,改用 additive(附加式)變動策略,在單一、穩定的版本中維持 backward compatibility。

如果預計在「未來任何時刻」都有可能出現 breaking changes 與更新,最好建立版本管理系統。在一開始就建立版本管理系統需要付出的成本比之後迫切需要它的時候才加入低得多。

Auth

把 authentication 資訊寫在 ~/.pgpass,格式如下:

1
host:port:database:user:password

在用 pg_dumppsql 時就可以不用輸入密碼。

當然這個檔案的權限要是 600

Backup

1
2
3
4
5
6
7
8
$ pg_dump \
--dbname="[DBNAME]" \
--file="[FILEPATH]" \
--inserts --create \
-h [HOST] \
-p [PORT] \
-U [USER] \
-w

Restore

1
psql -d [DBNAME] -f [SQL_FILE_PATH] -h [HOST] -p [PORT] -U [USER]

ECR 全名是 Elastic Container Registry,是 Amazon 的 docker container registry。

安裝 AWS CLI 後先 aws configure 設定

Push

  1. 做個 docker image
  2. authenticate
    $ aws ecr get-login-password --region [region] | sudo docker login --username AWS --password-stdin [AWSUserID].dkr.ecr.[region].amazonaws.com
  3. create repository,例如 hello-ecr
  4. 幫 image 上 tag,例如 [AWSUserID].dkr.ecr.us-east-2.amazonaws.com/hello-ecr:latest
  5. docker push

Run

一樣先 login,接著 docker run,例如:$ docker run [AWSUserID].dkr.ecr.us-east-2.amazonaws.com/hello-ecr:latest

Ref

在 Go 裡,method 的 receiver 是用 *Obj 還是用 Obj 會有不同的行為。

來個例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
type Vertex struct {
X, Y float64
}

func (v Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}

func (v Vertex) moveX(movement float64) {
v.X += movement
}

func (v *Vertex) moveY(movement float64) {
v.Y += movement
}

func main() {
v := Vertex{X: 10, Y: 20}

log.Println(v.X, v.Y, v.Abs()) // 10 20 22.360679774997898

v.moveX(2)
log.Println(v.X, v.Y, v.Abs()) // 10 20 22.360679774997898

v.moveY(3)
log.Println(v.X, v.Y, v.Abs()) // 10 23 25.079872407968907
}

v 可以看成像參數。

Vertex 就是 copy by value,caller 跟 callee 的 Vertex instance 是不同的。

*Vertex 就像 C 語言 pointer 參數,本質上還是 copy by value 但因為是 pointer,所以在 moveY() 中的 v 變成是指向 caller 的那個 Vertex instance。

基本上 method 會動到 struct 內的 field 內容都會用 pointer。習慣上當有一個 method 的 receiver 是用 pointer 時,所有 method 的 receiver 都會用 pointer。

環境

  • Synology NAS 型號:DS920+
  • DSM 版本:DSM 6.2.4-25556

建立 Private Docker Registry 步驟

  1. download registry image from Docker hub
  2. start registry container
  3. ssh 進 NAS
  4. /var/packages/Docker/etc/ 編輯 dockerd.json
  5. 加入 "insecure-registries":["host:port"]
  6. 重新啟動 docker (我是把套件停用再啟用啦…)