Skip to content

Commit

Permalink
feat(middlewares/acl/rbac): 添加对资源列表的读取方法
Browse files Browse the repository at this point in the history
  • Loading branch information
caixw committed Mar 13, 2024
1 parent eb19107 commit f78e293
Show file tree
Hide file tree
Showing 6 changed files with 196 additions and 63 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ require (
github.com/issue9/cache v0.10.0
github.com/issue9/web v0.87.6
github.com/shirou/gopsutil/v3 v3.24.2
golang.org/x/text v0.14.0
)

require (
Expand Down Expand Up @@ -39,7 +40,6 @@ require (
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Expand Down
56 changes: 38 additions & 18 deletions middlewares/acl/rbac/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@
// SPDX-License-Identifier: MIT

// Package rbac RBAC 的简单实现
//
// rbac := rbac.New(...)
// group := rbac.NewGroup("user", web.Phrase("user"))
//
// view := group.New("view", web.Phrase("view info")) // 返回判断权限的中间件
// del := group.New("del", web.Phrase("delete user")) // 返回判断权限的中间件
// router.Get("/users", view(func(*web.Context)web.Responser{
// // do somthing
// }))
//
// router.Delete("/users/{id}", del(func(*web.Context)web.Responser{
// // do somthing
// }))
package rbac

import (
Expand All @@ -15,14 +28,17 @@ import (
//
// T 表示的是用户的 ID 类型。
type RBAC[T comparable] struct {
s web.Server
super T // 超级管理员,不受权限组限制
store Store[T]
info *web.Logger
getUID GetUIDFunc[T]
resources map[string]*Resources[T]
roles map[string]*Role[T]
rolesMux *sync.RWMutex
s web.Server
super T // 超级管理员,不受权限组限制
store Store[T]
info *web.Logger
getUID GetUIDFunc[T]

resources []string // 缓存的所有资源项
groups map[string]*Group[T]

roles map[string]*Role[T]
rolesMux *sync.RWMutex
}

// GetUIDFunc 从 [web.Context] 获得当前的登录用户 ID
Expand All @@ -34,25 +50,29 @@ type GetUIDFunc[T comparable] func(*web.Context) (T, web.Responser)
// info 用于输出一些提示信息,比如权限的判断依据等;
// getUID 参考 [GetUIDFunc];
func New[T comparable](s web.Server, super T, store Store[T], info *web.Logger, getUID GetUIDFunc[T]) (*RBAC[T], error) {
// TODO 定时加载?

roles, err := store.Load()
if err != nil {
return nil, err
}

rbac := &RBAC[T]{
s: s,
super: super,
store: store,
info: info,
getUID: getUID,
resources: make(map[string]*Resources[T], 50),
rolesMux: &sync.RWMutex{},
}
s: s,
super: super,
store: store,
info: info,
getUID: getUID,

for _, role := range roles {
resources: make([]string, 0, 100),
groups: make(map[string]*Group[T], 50),

roles: roles,
rolesMux: &sync.RWMutex{},
}
for _, role := range rbac.roles {
role.rbac = rbac
}
rbac.roles = roles

return rbac, nil
}
Expand Down
104 changes: 82 additions & 22 deletions middlewares/acl/rbac/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,39 @@ package rbac

import (
"fmt"
"slices"
"strings"

"github.com/issue9/web"
"golang.org/x/text/message"
)

// Resources 表示一组资源
type Resources[T comparable] struct {
rbac *RBAC[T]
id string
title web.LocaleStringer
resources map[string]web.LocaleStringer
const idSeparator = '_'

// Group 表示一组资源
type Group[T comparable] struct {
rbac *RBAC[T]
id string
title web.LocaleStringer
items map[string]web.LocaleStringer
}

const idSeparator = '_'
type Resource struct {
ID string `json:"id" xml:"id,attr" yaml:"id"`
Title string `json:"title" xml:"title" yaml:"title"`
Items []*Resource `json:"items,omitempty" xml:"items>item,omitempty" yaml:"items,omitempty"`
}

// RoleResource 表示某个角色所能访问的资源
type RoleResource struct {
// Current 角色当前能访问的资源
Current []string `json:"current" xml:"current" yaml:"current"`

// Parent 角色的父类能访问的资源
//
// Parent 必然是包含了 Current 的所有值。
Parent []string `json:"parent" xml:"parent" yaml:"parent"`
}

// resourceExists 指定的资源 ID 是否存在
func (rbac *RBAC[T]) resourceExists(id string) bool {
Expand All @@ -29,42 +48,45 @@ func (rbac *RBAC[T]) resourceExists(id string) bool {
}

gid := id[:index]
if g, found := rbac.resources[gid]; found {
_, f := g.resources[id]
if g, found := rbac.groups[gid]; found {
_, f := g.items[id]
return f
}
return false
}

// NewResources 声明一组资源
// NewGroup 声明一组资源
//
// id 为该资源组的唯一 ID;
// title 对该资源组的描述;
func (rbac *RBAC[T]) NewResources(id string, title web.LocaleStringer) *Resources[T] {
if _, found := rbac.resources[id]; found {
func (rbac *RBAC[T]) NewGroup(id string, title web.LocaleStringer) *Group[T] {
if _, found := rbac.groups[id]; found {
panic(fmt.Sprintf("已经存在同名的资源组 %s", id))
}

res := &Resources[T]{
rbac: rbac,
id: id,
title: title,
resources: make(map[string]web.LocaleStringer, 10),
res := &Group[T]{
rbac: rbac,
id: id,
title: title,
items: make(map[string]web.LocaleStringer, 10),
}
rbac.resources[id] = res
rbac.groups[id] = res
return res
}

func joinID(gid, id string) string { return gid + string(idSeparator) + id }

// New 添加新的资源
//
// 返回的是用于判断是否拥有当前资源权限的中间件。
func (r *Resources[T]) New(id string, desc web.LocaleStringer) web.MiddlewareFunc {
id = r.id + string(idSeparator) + id
func (r *Group[T]) New(id string, desc web.LocaleStringer) web.MiddlewareFunc {
id = joinID(r.id, id)

if _, found := r.resources[id]; found {
if _, found := r.items[id]; found {
panic(fmt.Sprintf("已经存在同名的资源 %s", id))
}
r.resources[id] = desc
r.items[id] = desc
r.rbac.resources = append(r.rbac.resources, id)

return func(next web.HandlerFunc) web.HandlerFunc {
return func(ctx *web.Context) web.Responser {
Expand All @@ -88,3 +110,41 @@ func (r *Resources[T]) New(id string, desc web.LocaleStringer) web.MiddlewareFun
}
}
}

// Resources 所有资源的列表
func (rbac *RBAC[T]) Resources(p *message.Printer) []*Resource {
res := make([]*Resource, 0, len(rbac.groups))
for _, role := range rbac.groups {
items := make([]*Resource, 0, len(role.items))
for id, item := range role.items {
items = append(items, &Resource{ID: id, Title: item.LocaleString(p)})
}

res = append(res, &Resource{
ID: role.id,
Title: role.title.LocaleString(p),
Items: items,
})
}
return res
}

// Resource 当前角色的资源信息
func (role *Role[T]) Resource() *RoleResource {
var parent []string
if role.parent == nil {
parent = role.rbac.resources
} else {
parent = role.parent.Resources
}

var current []string
if len(role.Resources) > 0 {
current = role.Resources
}

return &RoleResource{
Current: slices.Clone(current),
Parent: slices.Clone(parent),
}
}
71 changes: 62 additions & 9 deletions middlewares/acl/rbac/resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,20 @@ import (
"github.com/issue9/assert/v4"
"github.com/issue9/web"
"github.com/issue9/web/server/servertest"
"golang.org/x/text/language"
"golang.org/x/text/message"
)

func TestRBAC_NewResources(t *testing.T) {
func TestRBAC_NewGroup(t *testing.T) {
a := assert.New(t, false)
s := newServer(a)
rbac, err := New(s, "", NewCacheStore[string](s, "c_"), s.Logs().INFO(), func(*web.Context) (string, web.Responser) { return "1", nil })
a.NotError(err).NotNil(rbac)

group := rbac.NewResources("id", web.Phrase("test"))
group := rbac.NewGroup("id", web.Phrase("test"))
a.NotNil(group).
PanicString(func() {
rbac.NewResources("id", web.Phrase("test"))
rbac.NewGroup("id", web.Phrase("test"))
}, "已经存在同名的资源组 id")
}

Expand All @@ -32,15 +34,15 @@ func RBAC_resourceExists(t *testing.T) {
rbac, err := New(s, "", NewCacheStore[string](s, "c_"), s.Logs().INFO(), func(*web.Context) (string, web.Responser) { return "1", nil })
a.NotError(err).NotNil(rbac)

g1 := rbac.NewResources("g1", nil)
g1 := rbac.NewGroup("g1", nil)
g1.New("1", nil)
g1.New("2", nil)
g2 := rbac.NewResources("g2", nil)
g2 := rbac.NewGroup("g2", nil)
g2.New("3", nil)
g2.New("4", nil)
a.True(rbac.resourceExists(g1.id + string(idSeparator) + "1")).
True(rbac.resourceExists(g2.id + string(idSeparator) + "3")).
False(rbac.resourceExists(g2.id + string(idSeparator) + "1"))
a.True(rbac.resourceExists(joinID(g1.id, "1"))).
True(rbac.resourceExists(joinID(g2.id, "3"))).
False(rbac.resourceExists(joinID(g2.id, "1")))
}

func TestResources_New(t *testing.T) {
Expand All @@ -56,7 +58,7 @@ func TestResources_New(t *testing.T) {
})
a.NotError(err).NotNil(rbac)

group := rbac.NewResources("id", web.Phrase("test"))
group := rbac.NewGroup("id", web.Phrase("test"))
a.NotNil(group)

m1 := group.New("id1", web.Phrase("desc"))
Expand All @@ -80,3 +82,54 @@ func TestResources_New(t *testing.T) {
// forbidden
servertest.Get(a, "http://localhost:8080/test?id=forbidden").Do(nil).Status(http.StatusForbidden)
}

func TestRBAC_Resources(t *testing.T) {
a := assert.New(t, false)
s := newServer(a)
rbac, err := New(s, "", NewCacheStore[string](s, "c_"), s.Logs().INFO(), func(*web.Context) (string, web.Responser) { return "1", nil })
a.NotError(err).NotNil(rbac)

g1 := rbac.NewGroup("g1", web.Phrase("test"))
g1.New("id1", web.Phrase("id1"))
g1.New("id2", web.Phrase("id2"))
g2 := rbac.NewGroup("g2", web.Phrase("test"))
g2.New("id1", web.Phrase("id1"))
g2.New("id2", web.Phrase("id2"))

a.Length(rbac.Resources(message.NewPrinter(language.SimplifiedChinese)), 2)
}

func TestRole_Resource(t *testing.T) {
a := assert.New(t, false)
s := newServer(a)
rbac, err := New(s, "", NewCacheStore[string](s, "c_"), s.Logs().INFO(), func(*web.Context) (string, web.Responser) { return "1", nil })
a.NotError(err).NotNil(rbac)

g1 := rbac.NewGroup("g1", web.Phrase("test"))
g1.New("id1", web.Phrase("id1"))
g1.New("id2", web.Phrase("id2"))
g2 := rbac.NewGroup("g2", web.Phrase("test"))
g2.New("id1", web.Phrase("id1"))
g2.New("id2", web.Phrase("id2"))

r1, err := rbac.Add("r1", "r1 desc", "")
a.NotError(err).NotNil(r1)
r1.Allow(joinID(g1.id, "id1"), joinID(g2.id, "id2"))

r2, err := rbac.Add("r2", "r2 desc", r1.ID)
a.NotError(err).NotNil(r2)

a.Equal(r1.Resource(), &RoleResource{
Current: []string{joinID(g1.id, "id1"), joinID(g2.id, "id2")},
Parent: []string{ // 没有父角色,则用全局的。
joinID(g1.id, "id1"),
joinID(g1.id, "id2"),
joinID(g2.id, "id1"),
joinID(g2.id, "id2"),
},
})

a.Equal(r2.Resource(), &RoleResource{
Parent: []string{joinID(g1.id, "id1"), joinID(g2.id, "id2")},
})
}
4 changes: 2 additions & 2 deletions middlewares/acl/rbac/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,10 @@ func (rbac *RBAC[T]) Add(name, desc, parent string) (*Role[T], error) {
return role, nil
}

// SetResources 关联角色与资源
// Allow 关联角色与资源
//
// 替换之前关联的资源。如果传递空值,将直接清空 [Role.Resources]
func (role *Role[T]) SetResources(res ...string) error {
func (role *Role[T]) Allow(res ...string) error {
if role.parent == nil {
for _, resID := range res { // res 是否真实存在
if !role.rbac.resourceExists(resID) {
Expand Down
Loading

0 comments on commit f78e293

Please sign in to comment.