最近公司有如下需求:
需求:
- 權限有多個,要可動態增減 (假設互相獨立,例如先不要有 PERMISSION_ALL 包含 PERMISSION_READ、PERMISSION_WRITE 這種)
- 條件有多個,要可動態增減,條件可以有優先層級關係
- 假設現在有一個權限叫做 PERMISSION_BLOG_ARTICLE_EDIT。
- 假設 user 的屬性有 accountId, groupId,user 只會屬於一個 group,group 可包含多個 user。
- 可以設定 accountId = x 時 Allow 或 Deny 權限,groupId = x 時 Allow 或 Deny 權限。
-
在檢查權限時,設定 accountId 層級比 groupId 大,會先檢查 accountId
有沒有權限,如果有權限 (Allow) 就有權限,如果沒有權限 (Deny) 就沒有權限,
如果不確定的話 (代表此權限沒有設定 accountId 相關的規則) 就去檢查 groupId 有沒有權限。
如果所有要檢查的屬性都檢查完了還是不確定是否有權限,視為沒有權限。
網上查到了 屬性型存取控制 (Attribute-based Access Control , ABAC),並以此為靈感,最後以以下方式實作了並在此紀綠分享:
Database 的 Table 可以如下設定 (以 PostgreSQL 為例):
-- 各種 permission CREATE TABLE permission ( id SERIAL PRIMARY KEY, -- permission id label VARCHAR NOT NULL -- 為 permission 取個名字方便辨視 ); -- permission 的 權限設定細節 CREATE TABLE permission_assignment_condition ( permission_assignee_type_id INT, -- 代表對哪一個 user 屬性去設定 rule is_equal_assignee_type_value BOOLEAN, -- 代表 user 屬性要 "等於" 還是 "不等於" assignee_type_value assignee_type_value INT, -- 代表 user 屬性要 "等於" 還是 "不等於" 某個值 is_allowed_permission BOOLEAN, -- 代表此規則是 Allow 還是 Deny 權限 permission_id INT, -- 此規則是對哪一個權限做設定 PRIMARY KEY(permission_assignee_type_id, is_equal_assignee_type_value, assignee_type_value, permission_id) ); -- user 的各種屬性,例如 accountId, groupId 等 CREATE TABLE permission_assignee_type ( id SERIAL PRIMARY KEY, -- 屬性 id label VARCHAR NOT NULL -- 為屬性取名方便辨視 );
如果想要設定:
- groupId = 3 對 PERMISSION_BLOG_ARTICLE_EDIT 為 Deny (就是 groupId = 3 沒有權限)。
- accountId = 100 對 PERMISSION_BLOG_ARTICLE_EDIT 為 Allow (但是 accountId 有權限,即使他的 groupId = 3 也是有權限,也就是 accountId 的層級比 groupId 大)。
我們就可以這樣設定:
-- 設定 permission
INSERT INTO permission(id, label) VALUES(1, 'PERMISSION_BLOG_ARTICLE_EDIT');
-- 設定 permission 規則要判斷的屬性
INSERT INTO permission_assignee_type(id, label) VALUES(1, accountId);
INSERT INTO permission_assignee_type(id, label) VALUES(2, groupId);
--設定 permission 的判斷規則
--設定 accountId = 100 ALLOW PERMISSION_BLOG_ARTICLE_EDIT
INSERT INTO permission_assignment_condition('permission_assignee_type_id',
'is_equal_assignee_type_value',
'assignee_type_value',
'is_allowed_permission',
'permission_id')
VALUES(1, true, 100, true, 1);
--設定 groupId = 3 DENY PERMISSION_BLOG_ARTICLE_EDIT
INSERT INTO permission_assignment_condition('permission_assignee_type_id',
'is_equal_assignee_type_value',
'assignee_type_value',
'is_allowed_permission',
'permission_id')
VALUES(2, true, 3, false, 1);
有了設定好的資料後,我們就可以用程式來判斷一個 User 是否有特定的權限,以下由 Java 來做例子,只要執行 PermissionDAO 的 isUserHasPermission(UserBean user, PERMISSION permission) 就可以得知此 User 有沒有被授權特別 permission 的權限:
UserBean.java:
package bean;
import java.util.Date;
public class UserBean {
int accountId;
int groupId;
public int getAccountId() {
return accountId;
}
public void setAccountId(int accountId) {
this.accountId = accountId;
}
public int getGroupId() {
return groupId;
}
public void setGroupId(int groupId) {
this.groupId = groupId;
}
}PERMISSION.java :
package constant;
public enum PERMISSION {
PERMISSION_BLOG_ARTICLE_EDIT(1, "PERMISSION_BLOG_ARTICLE_EDIT");
private int id;
private String label;
private PERMISSION(int id, String label) {
this.id = id;
this.label = label;
}
public static PERMISSION getPermissionById(int id, PERMISSION defaultPermission) {
for (PERMISSION value : values()) {
if (value.id == id) {
return value;
}
}
return defaultPermission;
}
public static PERMISSION getPermissionByLabel(String label, PERMISSION defaultPermission) {
for (PERMISSION value : values()) {
if (value.label.equalsIgnoreCase(label)) {
return value;
}
}
return defaultPermission;
}
public int getId() {
return id;
}
public String getLabel() {
return label;
}
}
PERMISSION_ALLOW_STATUS.java
package constant;
public enum PERMISSION_ALLOW_STATUS {
NO_DECISION,
ALLOW,
DENY;
}
PERMISSION_ASSIGNEE_TYPE.java
package constant;
public enum PERMISSION_ASSIGNEE_TYPE {
USER_ACCOUNT_ID(1, "USER_ACCOUNT_ID"),
USER_GROUP_ID(2, "USER_GROUP_ID")
private int id;
private String label;
private PERMISSION_ASSIGNEE_TYPE(int id, String label) {
this.id = id;
this.label = label;
}
public static PERMISSION_ASSIGNEE_TYPE getPermissionAssigneeTypeById(int id, PERMISSION_ASSIGNEE_TYPE defaultPermissionAssigneeType) {
for (PERMISSION_ASSIGNEE_TYPE value : values()) {
if (value.id == id) {
return value;
}
}
return defaultPermissionAssigneeType;
}
public static PERMISSION_ASSIGNEE_TYPE getPermissionAssigneeTypeByLabel(String label, PERMISSION_ASSIGNEE_TYPE defaultPermissionAssigneeType) {
for (PERMISSION_ASSIGNEE_TYPE value : values()) {
if (value.label.equalsIgnoreCase(label)) {
return value;
}
}
return defaultPermissionAssigneeType;
}
public int getId() {
return id;
}
public String getLabel() {
return label;
}
}
PermissionDAO.java
package dao;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import bean.UserBean;
import bean.PermissionAssignmentConditionBean;
import constant.PERMISSION;
import constant.PERMISSION_ALLOW_STATUS;
import constant.PERMISSION_ASSIGNEE_TYPE;
@Repository
public class PermissionDAO {
private JdbcTemplate jdbctemplate;
@Autowired
public PermissionDAO(JdbcTemplate jdbctemplate) {
this.jdbctemplate = jdbctemplate;
}
public boolean isUserHasPermission(UserBean user, PERMISSION permission) {
PERMISSION_ALLOW_STATUS permissionAllowStatus;
//取得特定 permission 的授權規則
List<PermissionAssignmentConditionBean> permissionAssignmentConditionList = getPermissionAssignmentCondition(permission);
//檢查 permission 授權規則對於此 accountId 符合 Allow, Deny, 還是 還未知 (NO_DECISION)
permissionAllowStatus = getPermissionAllowStatusByAssigneeType(permissionAssignmentConditionList,
permission,
PERMISSION_ASSIGNEE_TYPE.USER_ACCOUNT_ID,
user.getAccountId());
//如果不是 NO_DECISION ,即代表確定是 ALLOW 或 DENY,即可回傳確定的授權結果。
if (permissionAllowStatus != PERMISSION_ALLOW_STATUS.NO_DECISION) {
return permissionAllowStatus == PERMISSION_ALLOW_STATUS.ALLOW;
}
//如果對於 accountId 的授權是未知狀態 (即授權規則裡沒特別設定),就再檢查 groupId
permissionAllowStatus = getPermissionAllowStatusByAssigneeType(permissionAssignmentConditionList,
permission,
PERMISSION_ASSIGNEE_TYPE.USER_GROUP_ID,
user.getGroupId());
//跟檢查 accountId 一樣,如果不是 NO_DECISION ,即代表確定是 ALLOW 或 DENY,即可回傳確定的授權結果。
if (permissionAllowStatus != PERMISSION_ALLOW_STATUS.NO_DECISION) {
return permissionAllowStatus == PERMISSION_ALLOW_STATUS.ALLOW;
}
//如果已檢查全部要檢查的屬性,但結果還是 NO_DECISION 時,就視為沒有權限 (DENY)
return false;
}
public List<PermissionAssignmentConditionBean> getPermissionAssignmentCondition(PERMISSION permission) {
//將特定 permission 的授權規則查出來
String sql = "SELECT * FROM permission_assignment_condition WHERE permission_id = ?";
return jdbctemplate.query(sql, new RowMapper<PermissionAssignmentConditionBean>() {
@Override
public PermissionAssignmentConditionBean mapRow(ResultSet rs, int rowNum) throws SQLException {
PermissionAssignmentConditionBean permissionAssignmentCondition = new PermissionAssignmentConditionBean();
permissionAssignmentCondition.setPermissionAssigneeType(rs.getInt("permission_assignee_type_id"));
permissionAssignmentCondition.setIsEqualAssigneeTypeValue(rs.getBoolean("is_equal_assignee_type_value"));
permissionAssignmentCondition.setAssigneeTypeValue(rs.getInt("assignee_type_value"));
permissionAssignmentCondition.setIsAllowedPermission(rs.getBoolean("is_allowed_permission"));
permissionAssignmentCondition.setPermission(rs.getInt("permission_id"));
return permissionAssignmentCondition;
}
}, permission.getId());
}
private PERMISSION_ALLOW_STATUS getPermissionAllowStatusByAssigneeType(List<PermissionAssignmentConditionBean> permissionAssignmentConditionList,
PERMISSION permission,
PERMISSION_ASSIGNEE_TYPE permissionAssigneeType,
int assigneeValue) {
for (PermissionAssignmentConditionBean condition : permissionAssignmentConditionList) {
//濾掉其他沒有要檢查的 permission (包括 permission 不對 或 permissionAssigneeType 屬性不對)
if (permission != condition.getPermission() ||
permissionAssigneeType != condition.getPermissionAssigneeType()) {
continue;
}
//如果授權規則是要屬性值要 "等於" 某值
if (condition.getIsEqualAssigneeTypeValue()) {
//如果屬性值的確等於某值,回傳規則設定的 ALLOW 或 DENY
if (assigneeValue == condition.getAssigneeTypeValue()) {
return condition.getIsAllowedPermission() ? PERMISSION_ALLOW_STATUS.ALLOW : PERMISSION_ALLOW_STATUS.DENY;
}
//如果屬性值不等於某值,並不代表一定是 DENY,而是 NO_DECISION
continue;
}
//如果授權規則是要屬性值要 "不等於" 某值
//且屬性值的確不等於某值,回傳規則設定的 ALLOW 或 DENY
if (assigneeValue != condition.getAssigneeTypeValue()) {
return condition.getIsAllowedPermission() ? PERMISSION_ALLOW_STATUS.ALLOW : PERMISSION_ALLOW_STATUS.DENY;
}
//如果屬性值等於某值,並不代表一定是 DENY,而是 NO_DECISION
}
//找不到相應的 permission + permissionAssignmentType ,視為 NO_DECISION
return PERMISSION_ALLOW_STATUS.NO_DECISION;
}
}
這樣的設計也具有擴充性,日後可以依需求增加更多的 permission 和 permission_assignee_type,只要再增加資料進 PERMISSION 和 PERMISSION_ASSIGNEE_TYPE 的 Database Table 即可。
沒有留言 :
張貼留言