Camas 是我最近在處理專案中的許可權時寫的一個極簡許可權管理庫。API 的設計參考自一個 Ruby 的許可權管理庫 Pundit。

定義許可權規則

整個庫圍繞 Policy 的概念來定義許可權。假設我們有一篇文章只允許當前使用者是管理員或者文章還沒釋出的時候才允許編輯,那麼我們可以針對

post

這個模型定義這樣一個 Policy:

class

PostPolicy

{

constructor

context

{

this

user

=

context

user

}

canEdit

post

{

return

this

user

isAdmin

||

post

isPublished

}

}

可以看到,這是一個很純粹的類,這個類在例項化時會接受一個

context

作為引數,當前使用者的資訊等影響許可權判斷的內容都會放在

context

裡。然後我們定義了一個

canEdit

方法,這個方法接受當前的文章作為引數,並且返回一個

boolean

來確定該文章能否被編輯。

當然,在 Policy 定義多了以後我們可以抽取一個基類來處理

context

class

BasePolicy

{

constructor

context

{

this

user

=

context

user

}

}

class

PostPolicy

extends

BasePolicy

{

canEdit

post

{

return

this

user

isAdmin

||

post

isPublished

}

}

使用 Policy

在使用 Policy 之前我們需要先在 React 應用的最外層增加一個 Provider 來初始化

context

import

{

Provider

}

from

‘camas’

const

App

=

()

=>

{

const

currentUser

=

useCurrentUser

();

// 獲取當前使用者資訊的 hook,這裡省略實現

return

<

Provider

context

=

{{

user

currentUser

}}

>

<

Routes

/>

<

/Provider>

};

使用 Hook

import

{

usePolicy

}

from

‘camas’

const

PostList

=

({

posts

})

=>

{

const

postPolicy

=

usePolicy

PostPolicy

);

return

<

div

>

<

ul

>

{

posts

map

post

=>

<

li

>

{

post

title

}

{

postPolicy

canEdit

post

&&

<

span

>

Edit

<

/span>}

<

/li>

))}

<

/ul>

<

/div>

);

};

usePolicy

這個 hook 會去例項化 Policy,並且注入

context

,返回值就是 Policy 的例項。

使用元件

import

{

Authorize

}

from

‘camas’

const

PostList

=

({

posts

})

=>

{

return

<

div

>

<

ul

>

{

posts

map

post

=>

<

li

>

{

post

title

}

<

Authorize

with

=

{

PostPolicy

}

if

=

{

policy

=>

policy

canEdit

post

)}

>

<

span

>

Edit

<

/span>

<

/Authorize>

<

/li>

))}

<

/ul>

<

/div>

);

};

同樣的,

Authorize

會例項化 Policy 並注入

context

,然後你可以在

if

這個回撥函式上拿到 Policy 的例項。

HOC 注入

如果你是用類元件的,Camas 還提供以 HOC 的方式注入 Policy:

import

{

withPolicies

}

from

‘camas’

@

withPolicies

({

postPolicy

PostPolicy

})

class

PostList

extends

React

Component

{

render

()

{

const

{

posts

postPolicy

}

=

this

props

return

<

div

>

<

ul

>

{

posts

map

post

=>

<

li

>

{

post

title

}

{

postPolicy

canEdit

post

&&

<

span

>

Edit

<

/span>}

<

/li>

))}

<

/ul>

<

/div>

);

}

}

可以看到,

withPolicies

接受一個 map 引數,這個 map 的 key 是注入的屬性名,value 就是要注入的 Policy。

許可權測試

因為 Policy 的定義就是一個很存粹的 JS 類,所以為 Policy 寫測試也極其簡單,這裡以 jest 為例:

import

PostPolicy

from

‘。/PostPolicy’

describe

‘PostPolicy’

()

=>

{

const

admin

=

{

isAdmin

true

};

const

normalUser

=

{

isAdmin

false

}

const

publishedPost

=

{

isPublished

true

}

const

unpublishedPost

=

{

isPublished

false

}

it

“denies access if post is published”

()

=>

{

expect

new

PostPolicy

({

user

normalUser

})。

canEdit

publishedPost

))。

toBe

false

);

});

it

“grants access if post is unpublished”

()

=>

{

expect

new

PostPolicy

({

user

normalUser

})。

canEdit

unpublishedPost

))。

toBe

true

);

});

it

“grants access if post is published and user is an admin”

()

=>

{

expect

new

PostPolicy

({

user

admin

})。

canEdit

publishedPost

))。

toBe

true

);

});

});

結語

整個 Camas 庫的實現和使用都非常簡單,在 gzip 後只有 800 多個位元組,另外對 TypeScript 也有完美的型別支援。