Camas - 極簡 React 許可權管理庫
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 也有完美的型別支援。