Go 的 RBAC/ABAC 授权库


Restrict 是一个授权库,它提供了 RBAC 和 ABAC 模型的混合,允许定义简单的基于角色的策略,同时在需要时提供细粒度的控制。它可以帮助您在业务逻辑之外强制执行访问策略,并以方便的方式表达它们。

概念
Restrict 有助于以声明方式构建简单但功能强大的访问策略。为了做到这一点,我们引入了以下概念:

  • Subject 主体- 想要执行某些操作的实体。需要实现Subject接口并提供指定角色的列表。主体通常是您域中的任何类型的用户或客户端。
  • Resource 资源- 作为操作目标的实体。需要实现Resource接口并提供唯一的资源名称。资源可以由您域中的任何实体或对象实现。
  • Action 动作——可以对给定的资源执行的任意操作。
  • Context 上下文——包含验证访问权限所需的任何附加数据的值的映射。
  • Condition 条件- 授予访问权限需要满足的要求。有几个内置条件,但可以添加任何自定义条件,只要它实现Condition接口即可。条件是表达更细粒度控制的方式。

基本用法

type User struct {
    ID string
}

// Subject interface implementation.
func (u *User) GetRoles() []string {
    return []string{
"User"}
}

type Conversation struct {
    ID            string
    CreatedBy     string
    Participants  []string
    MessagesCount int
    Active        bool
}

// Resource interface implementation.
func (c *Conversation) GetResourceName() string {
    return
"Conversation"
}


var policy = &restrict.PolicyDefinition{
    Roles: restrict.Roles{
        
"User": {
            Grants: restrict.GrantsMap{
                
"Conversation": {
                    &restrict.Permission{Action:
"read"},
                    &restrict.Permission{Action:
"create"},
                },
            },
        },
    },
}

func main() {
    
// Create an instance of PolicyManager, which will be responsible for handling given PolicyDefinition.
    
// You can use one of the built-in persistence adapters (in-memory or json/yaml file adapters), or provide your own.
    policyMananger, err := restrict.NewPolicyManager(adapters.NewInMemoryAdapter(policy), true)
    if err != nil {
        log.Fatal(err)
    }

    manager := restrict.NewAccessManager(policyMananger)

    if err = manager.Authorize(&restrict.AccessRequest{
        Subject:        &User{},
        Resource:       &Conversation{},
        Actions:        []string{
"delete"},
    }); err != nil {
        fmt.Println(err)
// access denied for Action: "delete" on Resource: "Conversation"
    }
}

策略
策略是对给定系统中应执行的访问规则的描述。它由一个角色映射组成,每个角色映射包含一组针对每个资源授予的权限,以及可在各种角色和资源下重复使用的权限预设。以下是策略的示例:

var policy = &restrict.PolicyDefinition{
    // 角色图。 密钥对应于系统中主体可以归属的角色。
    Roles: restrict.Roles{
        
"User": {
            
// Optional, human readable description.
            Description:
"This is a simple User role, with permissions for basic chat operations.",
            
// Map of Permissions per Resource.
            
// 授予映射可以为空,表示给定角色没有权限(但仍可继承一些权限)。
            Grants: restrict.GrantsMap{
                
"Conversation": {
                    
// Subject "User" can "read" any "Conversation".
                    &restrict.Permission{Action:
"read"},
                    
// Subject "User" can "create" a "Conversation".
                    &restrict.Permission{Action:
"create"},
                    
// Subject "User" can "update" ONLY a "Coversation" that was
                    
// created by it. Check "updateOwn" preset definition below.
                    &restrict.Permission{Preset:
"updateOwn"},
                    
// Subject "User" can "delete" ONLY inactive "Conversation".
                    &restrict.Permission{
                        Action:
"delete",
                        Conditions: restrict.Conditions{
                            
// EmptyCondition requires a value (described by ValueDescriptor)
                            
// to be empty (falsy) in order to grant the access.
                            
// In this example, we want Conversation.Active to be false.
                            &restrict.EmptyCondition{
                                ID:
"deleteActive",
                                Value: &restrict.ValueDescriptor{
                                    Source: restrict.ResourceField,
                                    Field:  
"Active",
                                },
                            },
                        },
                    },
                },
            },
        },
        
"Admin": {
            Description:
"This is an Admin role, with permissions to manage Users.",
            
// "Admin" can do everything "User" can.
            Parents: []string{
"User"},
            
// AND can also perform other operations that User itself
            
// is not allowed to do.
            Grants: restrict.GrantsMap{
                
// Please note that in order to make this work,
                
// User needs to implement Resource interface.
                
"User": {
                    
// Subject "Admin" can create a "User".
                    &restrict.Permission{Action:
"create"},
                },
            },
        },
    },
    
// 可重复使用的权限映射。 关键字对应的是预设的名称。
    PermissionPresets: restrict.PermissionPresets{
        
"updateOwn": &restrict.Permission{
            
// An action that given Permission allows to perform.
            Action:
"update",
            
// Optional Conditions that when defined, need to be satisfied in order
            
// to allow the access.
            Conditions: restrict.Conditions{
                
// EqualCondition requires two values (described by ValueDescriptors)
                
// to be equal in order to grant the access.
                
// In this example we want to check if Conversation.CreatedBy and User.ID
                
// are the same, meaning that Conversation was created by given User.
                &restrict.EqualCondition{
                    
// Optional ID helpful when we need to identify the exact Condition that failed
                    
// when checking the access.
                    ID:
"isOwner",
                    
// First value to compare.
                    Left: &restrict.ValueDescriptor{
                        Source: restrict.ResourceField,
                        Field:  
"CreatedBy",
                    },
                    
// Second value to compare.
                    Right: &restrict.ValueDescriptor{
                        Source: restrict.SubjectField,
                        Field:  
"ID",
                    },
                },
            },
        },
    },
}


访问请求
AccessRequest是一个描述有关访问的问题的对象——主体是否可以对特定资源执行给定的操作。

如果您只需要类似 RBAC 的功能,或者想要执行“初步”访问检查(例如,只检查主题是否可以读取给定的资源,而不考虑条件),则空的 Subject/Resource 实例就足够了。否则,应在授权之前检索主题和资源,并在 AccessRequest 中传递。

例如,在典型的后端应用程序中,主题(用户)可能来自请求上下文。资源(对话)可以从数据库中获取。整个授权过程以及主题/资源检索都可以在中间件功能中进行。

// ... manager setup

// Create empty instances or provide the correct entities.
user := &User{}
conversation := &Conversation{}

accessRequest := &restrict.AccessRequest{
    
// Subject - subject (typically a user) that wants to perform given Actions.
    
// Needs to implement Subject interface.
    
// Required.
    Subject: user,
    
// Resource - resource that given Subject wants to interact with.
    
// Needs to implement Resource interface.
    
// Required.
    Resource: conversation,
    
// Actions - list of operations Subject wants to perform on given Resource.
    
// Required.
    Actions:  []string{
"read", "create"},
    
// Context - map of any additional values needed while checking the Conditions.
    
// Optional.
    Context: restrict.Context{
        
"SomeField": "someValue",
    },
    
// SkipConditions - allows to skip Conditions while checking the access.
    
// Optional, default: false.
    SkipConditions: false,
    
// CompleteValidation - when true, validation will not return early, and all possible errors
    
// will be returned, including all Conditions checks.
    
// Optional, default: false.
    CompleteValidation bool
}

// If the access is granted, err will be nil - otherwise,
// an error will be returned containing information about the failure.
err = manager.Authorize(accessRequest)

除了空的 Subject/Resource 实例之外,还有两个辅助函数 -UseSubject()和UseResource(),当您不想创建空实例或给定的 Subject/Resource 未由域中的任何类型表示时,它们会很有用。在这种情况下,您可以使用:

accessRequest := &restrict.AccessRequest{
    Subject:  restrict.UseSubject("User"),
    Resource: restrict.UseResource(
"Conversation"),
    Actions:  []string{
"read", "create"},
}

访问管理器
AccessManager负责实际验证。设置了适当的PolicyManager实例后(有关详细信息,请参阅PolicyManager 和持久性),您可以使用其Authorize方法来检查给定的AccessRequest。Authorize如果未授予访问权限,则返回错误,nil否则(表示没有错误并且授予访问权限)。

var policy = &restrict.PolicyDefinition{
    // ... policy details
}

adapter := adapters.NewInMemoryAdapter(policy)
policyMananger, err := restrict.NewPolicyManager(adapter, true)
if err != nil {
    log.Fatal(err)
}

manager := restrict.NewAccessManager(policyMananger)

accessRequest := &restrict.AccessRequest{
    
// ... request details
}

// 
err := manager.Authorize(accessRequest)

验证和错误
由于Authorize方法依赖于各种操作,包括以条件形式提供的外部操作,因此其返回类型是通用error类型。但是,当错误是由实际策略验证(即未授予权限或不满足条件)导致时,Authorize将返回一个实例AccessDeniedError,该实例提供了有关拒绝访问原因的大量信息和上下文。它有助于轻松处理错误和调试。

如果错误是由策略验证引起的,则将根据请求中使用的验证策略返回。任何其他错误都将立即返回。

AccessDeniedError可以包含一个或多个潜在原因 - 类型的错误PermissionError,可通过Reasons属性访问。每个错误都PermissionError可以包含零个或多个ConditionNotSatisfiedError错误,可通过ConditionErrors属性访问。

可以使用类型断言来测试返回的错误。

err := manager.Authorize(accessRequest)
if accessError, ok := err.(*restrict.AccessDeniedError); ok {
    // Error() implementation. Returns a message in a form: "access denied for Action/s: ... on Resource: ..."
    fmt.Println(accessError)
    
// Returns an AccessRequest that failed.
    fmt.Println(accessError.Request)
    
// Returns first reason for the denied access.
    
// Especially helpful in fail-early mode, where there will only be one Reason.
    fmt.Println(accessError.FirstReason())
    
    
// Reasons property will hold all errors that caused the access to be denied.
    for _, permissionErr := range accessError.Reasons {
        fmt.Println(permissionErr)
        fmt.Println(permissionErr.Action)
        fmt.Println(permissionErr.RoleName)
        fmt.Println(permissionErr.ResourceName)
        
        
// Returns first ConditionNotSatisfied error for given PermissionError, if any was returned for given PermissionError.
        
// Especially helpful in fail-early mode, where there will only be one failed Condition.
        fmt.Println(permissionErr.FirstConditionError())
        
        
// ConditionErrors property will hold all ConditionNotSatisfied errors.
        for _, conditionErr := range permissionErr.ConditionErrors {
            fmt.Println(conditionErr)
            fmt.Println(conditionErr.Reason)
            
            
// Every ConditionNotSatisfied contains an instance of Condition that returned it,
            
// so it can be tested using type assertion to get more details about failed Condition.
            if emptyCondition, ok := conditionErr.Condition.(*restrict.EmptyCondition); ok {
                fmt.Println(emptyCondition.ID)
            }
        }
    }
}


目录