feat(users): add roles APIs (#63)

* feat(kvstore): add namespace operations for bool

* feat(userstore): add methods for roles

* chore(multiusers): remove useless todo

* feat(multiusers): add apis for roles

* test(roles): add e2e tests for role APIs

* test(e2e/files): enable files tests
This commit is contained in:
Hexxa 2021-07-10 07:08:32 -05:00 committed by GitHub
parent 4b6f6d9e1f
commit 9748d0cab4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 316 additions and 11 deletions

View file

@ -61,11 +61,50 @@ func (cl *SingleUserClient) AddUser(name, pwd, role string, token *http.Cookie)
}). }).
End() End()
if len(errs) > 0 {
return nil, nil, errs
}
auResp := &multiusers.AddUserResp{} auResp := &multiusers.AddUserResp{}
err := json.Unmarshal([]byte(body), auResp) err := json.Unmarshal([]byte(body), auResp)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
return nil, nil, errs return nil, nil, errs
} }
return resp, auResp, nil return resp, auResp, errs
}
func (cl *SingleUserClient) AddRole(role string, token *http.Cookie) (*http.Response, string, []error) {
return cl.r.Post(cl.url("/v1/roles/")).
AddCookie(token).
Send(multiusers.AddRoleReq{
Role: role,
}).
End()
}
func (cl *SingleUserClient) DelRole(role string, token *http.Cookie) (*http.Response, string, []error) {
return cl.r.Delete(cl.url("/v1/roles/")).
AddCookie(token).
Send(multiusers.DelRoleReq{
Role: role,
}).
End()
}
func (cl *SingleUserClient) ListRoles(token *http.Cookie) (*http.Response, *multiusers.ListRolesResp, []error) {
resp, body, errs := cl.r.Get(cl.url("/v1/roles/")).
AddCookie(token).
End()
if len(errs) > 0 {
return nil, nil, errs
}
lsResp := &multiusers.ListRolesResp{}
err := json.Unmarshal([]byte(body), lsResp)
if err != nil {
errs = append(errs, err)
return nil, nil, errs
}
return resp, lsResp, errs
} }

View file

@ -176,7 +176,6 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
c.JSON(q.ErrResp(c, 400, err)) c.JSON(q.ErrResp(c, 400, err))
return return
} }
// TODO: check privilege?
// TODO: do more comprehensive validation // TODO: do more comprehensive validation
// Role and duplicated name will be validated by the store // Role and duplicated name will be validated by the store
@ -209,6 +208,73 @@ func (h *MultiUsersSvc) AddUser(c *gin.Context) {
c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)}) c.JSON(200, &AddUserResp{ID: fmt.Sprint(uid)})
} }
type AddRoleReq struct {
Role string `json:"role"`
}
func (h *MultiUsersSvc) AddRole(c *gin.Context) {
req := &AddRoleReq{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
// TODO: do more comprehensive validation
if len(req.Role) < 2 {
c.JSON(q.ErrResp(c, 400, errors.New("name length must be greater than 2")))
return
}
err := h.deps.Users().AddRole(req.Role)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(q.Resp(200))
}
type DelRoleReq struct {
Role string `json:"role"`
}
func (h *MultiUsersSvc) DelRole(c *gin.Context) {
req := &DelRoleReq{}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(q.ErrResp(c, 400, err))
return
}
// TODO: do more comprehensive validation
if len(req.Role) < 2 {
c.JSON(q.ErrResp(c, 400, errors.New("name length must be greater than 2")))
return
}
err := h.deps.Users().DelRole(req.Role)
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(q.Resp(200))
}
type ListRolesReq struct{}
type ListRolesResp struct {
Roles map[string]bool `json:"roles"`
}
func (h *MultiUsersSvc) ListRoles(c *gin.Context) {
roles, err := h.deps.Users().ListRoles()
if err != nil {
c.JSON(q.ErrResp(c, 500, err))
return
}
c.JSON(200, &ListRolesResp{Roles: roles})
}
func (h *MultiUsersSvc) getUserInfo(c *gin.Context) (map[string]string, error) { func (h *MultiUsersSvc) getUserInfo(c *gin.Context) (map[string]string, error) {
tokenStr, err := c.Cookie(TokenCookie) tokenStr, err := c.Cookie(TokenCookie)
if err != nil { if err != nil {

View file

@ -77,10 +77,14 @@ func (bp *BoltPvd) Close() error {
} }
func (bp *BoltPvd) GetBool(key string) (bool, bool) { func (bp *BoltPvd) GetBool(key string) (bool, bool) {
return bp.GetBoolIn("bools", key)
}
func (bp *BoltPvd) GetBoolIn(ns, key string) (bool, bool) {
buf, ok := make([]byte, 1), false buf, ok := make([]byte, 1), false
bp.db.View(func(tx *bolt.Tx) error { bp.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("bools")) b := tx.Bucket([]byte(ns))
v := b.Get([]byte(key)) v := b.Get([]byte(key))
copy(buf, v) copy(buf, v)
ok = v != nil ok = v != nil
@ -92,23 +96,52 @@ func (bp *BoltPvd) GetBool(key string) (bool, bool) {
} }
func (bp *BoltPvd) SetBool(key string, val bool) error { func (bp *BoltPvd) SetBool(key string, val bool) error {
return bp.SetBoolIn("bools", key, val)
}
func (bp *BoltPvd) SetBoolIn(ns, key string, val bool) error {
var bVal byte = 0 var bVal byte = 0
if val { if val {
bVal = 1 bVal = 1
} }
return bp.db.Update(func(tx *bolt.Tx) error { return bp.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("bools")) b := tx.Bucket([]byte(ns))
return b.Put([]byte(key), []byte{bVal}) return b.Put([]byte(key), []byte{bVal})
}) })
} }
func (bp *BoltPvd) DelBool(key string) error { func (bp *BoltPvd) DelBool(key string) error {
return bp.DelBoolIn("bools", key)
}
func (bp *BoltPvd) DelBoolIn(ns, key string) error {
return bp.db.Update(func(tx *bolt.Tx) error { return bp.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("bools")) b := tx.Bucket([]byte(ns))
return b.Delete([]byte(key)) return b.Delete([]byte(key))
}) })
} }
func (bp *BoltPvd) ListBools() (map[string]bool, error) {
return bp.ListBoolsIn("bools")
}
func (bp *BoltPvd) ListBoolsIn(ns string) (map[string]bool, error) {
list := map[string]bool{}
err := bp.db.View(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(ns))
if b == nil {
return ErrBucketNotFound
}
b.ForEach(func(k, v []byte) error {
list[string(k)] = (v[0] == 1)
return nil
})
return nil
})
return list, err
}
func (bp *BoltPvd) GetInt(key string) (int, bool) { func (bp *BoltPvd) GetInt(key string) (int, bool) {
x, ok := bp.GetInt64(key) x, ok := bp.GetInt64(key)
return int(x), ok return int(x), ok
@ -195,7 +228,7 @@ func (bp *BoltPvd) ListInt64sIn(ns string) (map[string]int64, error) {
if n < 0 { if n < 0 {
return fmt.Errorf("fail to parse int64 for key (%s)", k) return fmt.Errorf("fail to parse int64 for key (%s)", k)
} }
list[fmt.Sprintf("%s", k)] = x list[string(k)] = x
return nil return nil
}) })
return nil return nil
@ -340,7 +373,7 @@ func (bp *BoltPvd) ListStringsIn(ns string) (map[string]string, error) {
} }
b.ForEach(func(k, v []byte) error { b.ForEach(func(k, v []byte) error {
kv[fmt.Sprintf("%s", k)] = fmt.Sprintf("%s", v) kv[string(k)] = string(v)
return nil return nil
}) })
return nil return nil

View file

@ -9,8 +9,13 @@ type IKVStore interface {
AddNamespace(nsName string) error AddNamespace(nsName string) error
DelNamespace(nsName string) error DelNamespace(nsName string) error
GetBool(key string) (bool, bool) GetBool(key string) (bool, bool)
GetBoolIn(ns, key string) (bool, bool)
SetBool(key string, val bool) error SetBool(key string, val bool) error
SetBoolIn(ns, key string, val bool) error
DelBool(key string) error DelBool(key string) error
DelBoolIn(ns, key string) error
ListBools() (map[string]bool, error)
ListBoolsIn(ns string) (map[string]bool, error)
GetInt(key string) (int, bool) GetInt(key string) (int, bool)
SetInt(key string, val int) error SetInt(key string, val int) error
DelInt(key string) error DelInt(key string) error

View file

@ -15,6 +15,7 @@ func TestKVStoreProviders(t *testing.T) {
var err error var err error
var ok bool var ok bool
key, boolV, intV, int64V, floatV, stringV := "key", true, 2027, int64(2027), 3.1415, "foobar" key, boolV, intV, int64V, floatV, stringV := "key", true, 2027, int64(2027), 3.1415, "foobar"
key2, boolV2 := "key2", false
kvstoreTest := func(store kvstore.IKVStore, t *testing.T) { kvstoreTest := func(store kvstore.IKVStore, t *testing.T) {
// test bools // test bools
@ -26,6 +27,19 @@ func TestKVStoreProviders(t *testing.T) {
if err != nil { if err != nil {
t.Errorf("there should be no error %v", err) t.Errorf("there should be no error %v", err)
} }
err = store.SetBool(key2, boolV2)
if err != nil {
t.Errorf("there should be no error %v", err)
}
boolList, err := store.ListBools()
if err != nil {
t.Errorf("there should be no error %v", err)
}
if boolList[key] != boolV {
t.Error("listBool incorrect val1")
} else if boolList[key2] != boolV2 {
t.Error("listBool incorrect val2")
}
boolVGot, ok := store.GetBool(key) boolVGot, ok := store.GetBool(key)
if !ok { if !ok {
t.Error("value should exit") t.Error("value should exit")

View file

@ -179,6 +179,11 @@ func initHandlers(router *gin.Engine, cfg gocfg.ICfg, deps *depidx.Deps) (*gin.E
usersAPI.PATCH("/pwd", userHdrs.SetPwd) usersAPI.PATCH("/pwd", userHdrs.SetPwd)
usersAPI.POST("/", userHdrs.AddUser) usersAPI.POST("/", userHdrs.AddUser)
rolesAPI := v1.Group("/roles")
rolesAPI.POST("/", userHdrs.AddRole)
rolesAPI.DELETE("/", userHdrs.DelRole)
rolesAPI.GET("/", userHdrs.ListRoles)
filesAPI := v1.Group("/fs") filesAPI := v1.Group("/fs")
filesAPI.POST("/files", fileHdrs.Create) filesAPI.POST("/files", fileHdrs.Create)
filesAPI.DELETE("/files", fileHdrs.Delete) filesAPI.DELETE("/files", fileHdrs.Delete)

View file

@ -17,7 +17,7 @@ import (
"github.com/ihexxa/quickshare/src/handlers/fileshdr" "github.com/ihexxa/quickshare/src/handlers/fileshdr"
) )
func xTestFileHandlers(t *testing.T) { func TestFileHandlers(t *testing.T) {
addr := "http://127.0.0.1:8686" addr := "http://127.0.0.1:8686"
root := "testData" root := "testData"
config := `{ config := `{

View file

@ -112,4 +112,62 @@ func TestSingleUserHandlers(t *testing.T) {
t.Fatal(resp.StatusCode) t.Fatal(resp.StatusCode)
} }
}) })
t.Run("test roles APIs: Login-AddRole-ListRoles-DelRole-ListRoles", func(t *testing.T) {
resp, _, errs := usersCl.Login(adminName, adminNewPwd)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
token := client.GetCookie(resp.Cookies(), su.TokenCookie)
roles := []string{"role1", "role2"}
for _, role := range roles {
resp, _, errs := usersCl.AddRole(role, token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
}
resp, lsResp, errs := usersCl.ListRoles(token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
for _, role := range append(roles, []string{
userstore.AdminRole,
userstore.UserRole,
userstore.VisitorRole,
}...) {
if !lsResp.Roles[role] {
t.Fatalf("role(%s) not found", role)
}
}
for _, role := range roles {
resp, _, errs := usersCl.DelRole(role, token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
}
resp, lsResp, errs = usersCl.ListRoles(token)
if len(errs) > 0 {
t.Fatal(errs)
} else if resp.StatusCode != 200 {
t.Fatal(resp.StatusCode)
}
for _, role := range roles {
if lsResp.Roles[role] {
t.Fatalf("role(%s) should not exist", role)
}
}
})
} }

View file

@ -21,6 +21,7 @@ const (
NamesNs = "users" NamesNs = "users"
PwdsNs = "pwds" PwdsNs = "pwds"
RolesNs = "roles" RolesNs = "roles"
RoleListNs = "roleList"
InitTimeKey = "initTime" InitTimeKey = "initTime"
) )
@ -40,6 +41,9 @@ type IUserStore interface {
SetName(id uint64, name string) error SetName(id uint64, name string) error
SetPwd(id uint64, pwd string) error SetPwd(id uint64, pwd string) error
SetRole(id uint64, role string) error SetRole(id uint64, role string) error
AddRole(role string) error
DelRole(role string) error
ListRoles() (map[string]bool, error)
} }
type KVUserStore struct { type KVUserStore struct {
@ -57,6 +61,7 @@ func NewKVUserStore(store kvstore.IKVStore) (*KVUserStore, error) {
PwdsNs, PwdsNs,
RolesNs, RolesNs,
InitNs, InitNs,
RoleListNs,
} { } {
if err = store.AddNamespace(nsName); err != nil { if err = store.AddNamespace(nsName); err != nil {
return nil, err return nil, err
@ -71,7 +76,8 @@ func NewKVUserStore(store kvstore.IKVStore) (*KVUserStore, error) {
} }
func (us *KVUserStore) Init(rootName, rootPwd string) error { func (us *KVUserStore) Init(rootName, rootPwd string) error {
err := us.AddUser(&User{ var err error
err = us.AddUser(&User{
ID: 0, ID: 0,
Name: rootName, Name: rootName,
Pwd: rootPwd, Pwd: rootPwd,
@ -81,6 +87,13 @@ func (us *KVUserStore) Init(rootName, rootPwd string) error {
return err return err
} }
for _, role := range []string{AdminRole, UserRole, VisitorRole} {
err = us.AddRole(role)
if err != nil {
return err
}
}
return us.store.SetStringIn(InitNs, InitTimeKey, fmt.Sprintf("%d", time.Now().Unix())) return us.store.SetStringIn(InitNs, InitTimeKey, fmt.Sprintf("%d", time.Now().Unix()))
} }
@ -244,3 +257,33 @@ func (us *KVUserStore) SetRole(id uint64, role string) error {
return us.store.SetStringIn(RolesNs, userID, role) return us.store.SetStringIn(RolesNs, userID, role)
} }
func (us *KVUserStore) AddRole(role string) error {
us.mtx.Lock()
defer us.mtx.Unlock()
_, ok := us.store.GetBoolIn(RoleListNs, role)
if ok {
return fmt.Errorf("role (%s) exists", role)
}
return us.store.SetBoolIn(RoleListNs, role, true)
}
func (us *KVUserStore) DelRole(role string) error {
us.mtx.Lock()
defer us.mtx.Unlock()
if role == AdminRole || role == UserRole || role == VisitorRole {
return errors.New("predefined roles can not be deleted")
}
return us.store.DelBoolIn(RoleListNs, role)
}
func (us *KVUserStore) ListRoles() (map[string]bool, error) {
us.mtx.Lock()
defer us.mtx.Unlock()
return us.store.ListBoolsIn(RoleListNs)
}

View file

@ -11,7 +11,7 @@ import (
func TestUserStores(t *testing.T) { func TestUserStores(t *testing.T) {
rootName, rootPwd := "root", "rootPwd" rootName, rootPwd := "root", "rootPwd"
testUserStore := func(t *testing.T, store IUserStore) { testUserMethods := func(t *testing.T, store IUserStore) {
root, err := store.GetUser(0) root, err := store.GetUser(0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -93,6 +93,47 @@ func TestUserStores(t *testing.T) {
} }
} }
testRoleMethods := func(t *testing.T, store IUserStore) {
roles := []string{"role1", "role2"}
var err error
for _, role := range roles {
err = store.AddRole(role)
if err != nil {
t.Fatal(err)
}
}
roleMap, err := store.ListRoles()
if err != nil {
t.Fatal(err)
}
for _, role := range append(roles, []string{
AdminRole, UserRole, VisitorRole,
}...) {
if !roleMap[role] {
t.Fatalf("role(%s) not found", role)
}
}
for _, role := range roles {
err = store.DelRole(role)
if err != nil {
t.Fatal(err)
}
}
roleMap, err = store.ListRoles()
if err != nil {
t.Fatal(err)
}
for _, role := range roles {
if roleMap[role] {
t.Fatalf("role(%s) should not exist", role)
}
}
}
t.Run("testing KVUserStore", func(t *testing.T) { t.Run("testing KVUserStore", func(t *testing.T) {
rootPath, err := ioutil.TempDir("./", "quickshare_userstore_test_") rootPath, err := ioutil.TempDir("./", "quickshare_userstore_test_")
if err != nil { if err != nil {
@ -111,6 +152,7 @@ func TestUserStores(t *testing.T) {
t.Fatal("fail to init kvstore", err) t.Fatal("fail to init kvstore", err)
} }
testUserStore(t, store) testUserMethods(t, store)
testRoleMethods(t, store)
}) })
} }