diff --git a/action.go b/action.go index 122ba1d..3b33ee1 100644 --- a/action.go +++ b/action.go @@ -1,6 +1,7 @@ package usermngmt import ( + "github.com/Selly-Modules/usermngmt/cache" "github.com/Selly-Modules/usermngmt/model" "github.com/Selly-Modules/usermngmt/permission" "github.com/Selly-Modules/usermngmt/role" @@ -33,8 +34,8 @@ func (s Service) ChangeUserStatus(userID, newStatus string) error { return user.ChangeUserStatus(userID, newStatus) } -// GetAllUser ... -func (s Service) GetAllUser(query model.UserAllQuery) model.UserAll { +// GetAllUsers ... +func (s Service) GetAllUsers(query model.UserAllQuery) model.UserAll { return user.All(query) } @@ -48,6 +49,11 @@ func (s Service) LoginWithEmailAndPassword(email, password string) (model.User, return user.LoginWithEmailAndPassword(email, password) } +// HasPermission ... +func (s Service) HasPermission(userID, permission string) bool { + return user.HasPermission(userID, permission) +} + // // Role // @@ -56,7 +62,11 @@ func (s Service) LoginWithEmailAndPassword(email, password string) (model.User, // CreateRole ... func (s Service) CreateRole(payload model.RoleCreateOptions) error { - return role.Create(payload) + if err := role.Create(payload); err != nil { + return err + } + cache.Roles() + return nil } // UpdateRole ... @@ -77,12 +87,20 @@ func (s Service) GetAllRoles(query model.RoleAllQuery) model.RoleAll { // CreatePermission ... func (s Service) CreatePermission(payload model.PermissionCreateOptions) error { - return permission.Create(payload) + if err := permission.Create(payload); err != nil { + return err + } + cache.Roles() + return nil } // UpdatePermission ... func (s Service) UpdatePermission(permissionID string, payload model.PermissionUpdateOptions) error { - return permission.Update(permissionID, payload) + if err := permission.Update(permissionID, payload); err != nil { + return err + } + cache.Roles() + return nil } // GetAllPermissions ... diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..5f51ac5 --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,29 @@ +package cache + +import ( + "log" + "time" + + "github.com/allegro/bigcache/v3" +) + +var mc *bigcache.BigCache + +// Init ... +func Init() { + // The time after which entries can be evicted is 30 days + const cacheTime = 24 * 30 * time.Hour // 30 days + c, err := bigcache.NewBigCache(bigcache.DefaultConfig(cacheTime)) + if err != nil { + log.Fatalf("Cannot init Cache %v", err) + } + mc = c + + // Cache roles + Roles() +} + +// GetInstance ... +func GetInstance() *bigcache.BigCache { + return mc +} diff --git a/cache/db.go b/cache/db.go new file mode 100644 index 0000000..6d062cb --- /dev/null +++ b/cache/db.go @@ -0,0 +1,64 @@ +package cache + +import ( + "context" + + "github.com/Selly-Modules/logger" + "github.com/Selly-Modules/usermngmt/database" + "github.com/Selly-Modules/usermngmt/model" + "go.mongodb.org/mongo-driver/mongo/options" +) + +func roleFindByCondition(ctx context.Context, cond interface{}, opts ...*options.FindOptions) (docs []model.DBRole) { + var ( + col = database.GetRoleCol() + ) + docs = make([]model.DBRole, 0) + + cursor, err := col.Find(ctx, cond, opts...) + if err != nil { + logger.Error("usermngmt - Role - Find", logger.LogData{ + "cond": cond, + "opts": opts, + "err": err.Error(), + }) + return + } + defer cursor.Close(ctx) + if err = cursor.All(ctx, &docs); err != nil { + logger.Error("usermngmt - Role - Decode", logger.LogData{ + "cond": cond, + "opts": opts, + "err": err.Error(), + }) + return + } + return +} + +func permissionFindByCondition(ctx context.Context, cond interface{}, opts ...*options.FindOptions) (docs []model.DBPermission) { + var ( + col = database.GetPermissionCol() + ) + docs = make([]model.DBPermission, 0) + + cursor, err := col.Find(ctx, cond, opts...) + if err != nil { + logger.Error("usermngmt - Permission - Find", logger.LogData{ + "cond": cond, + "opts": opts, + "err": err.Error(), + }) + return + } + defer cursor.Close(ctx) + if err = cursor.All(ctx, &docs); err != nil { + logger.Error("usermngmt - Permission - Decode", logger.LogData{ + "cond": cond, + "opts": opts, + "err": err.Error(), + }) + return + } + return +} diff --git a/cache/model.go b/cache/model.go new file mode 100644 index 0000000..96b64cb --- /dev/null +++ b/cache/model.go @@ -0,0 +1,8 @@ +package cache + +// CachedRole ... +type CachedRole struct { + Role string `json:"role"` + IsAdmin bool `json:"isAdmin"` + Permissions []string `json:"permissions"` +} diff --git a/cache/role.go b/cache/role.go new file mode 100644 index 0000000..7f08d5e --- /dev/null +++ b/cache/role.go @@ -0,0 +1,70 @@ +package cache + +import ( + "context" + "encoding/json" + "sync" + + "github.com/Selly-Modules/logger" + "github.com/Selly-Modules/usermngmt/model" + "go.mongodb.org/mongo-driver/bson" +) + +// Roles ... +func Roles() { + var ( + ctx = context.Background() + wg sync.WaitGroup + cond = bson.M{} + ) + + // Find + roles := roleFindByCondition(ctx, cond) + permissions := permissionFindByCondition(ctx, cond) + + wg.Add(len(roles)) + for _, value := range roles { + go func(role model.DBRole) { + defer wg.Done() + rolePermissions := make([]string, 0) + // Get role permissions + for _, permission := range permissions { + if permission.RoleID == role.ID { + rolePermissions = append(rolePermissions, permission.Code) + } + } + + // Cache Role + entry, _ := json.Marshal(CachedRole{ + Role: role.Code, + IsAdmin: role.IsAdmin, + Permissions: rolePermissions, + }) + if err := mc.Set(role.ID.Hex(), entry); err != nil { + logger.Error("usermngmt - CacheRole", logger.LogData{ + "err": err.Error(), + }) + return + } + }(value) + } + + wg.Wait() + return +} + +// GetCachedRole ... +func GetCachedRole(key string) CachedRole { + entry, err := mc.Get(key) + if err != nil { + Roles() + entry, _ = mc.Get(key) + } + var cachedRole CachedRole + if err = json.Unmarshal(entry, &cachedRole); err != nil { + logger.Error("usermngmt - GetCachedRole - Unmarshal", logger.LogData{ + "err": err.Error(), + }) + } + return cachedRole +} diff --git a/go.mod b/go.mod index 70aa30c..a2de3aa 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.17 require ( github.com/Selly-Modules/logger v0.0.0-20210809034923-140a51f39ec9 github.com/Selly-Modules/mongodb v0.0.0-20211013094205-a8ab24a96c4c + github.com/allegro/bigcache/v3 v3.0.1 + github.com/thoas/go-funk v0.9.1 go.mongodb.org/mongo-driver v1.7.4 + golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 ) require ( @@ -32,7 +35,6 @@ require ( go.uber.org/atomic v1.7.0 // indirect go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.18.1 // indirect - golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073 // indirect golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 // indirect golang.org/x/mod v0.3.0 // indirect golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect diff --git a/go.sum b/go.sum index 7fdc319..7bdbc2f 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/Selly-Modules/logger v0.0.0-20210809034923-140a51f39ec9 h1:AuJ/IIZ7yp github.com/Selly-Modules/logger v0.0.0-20210809034923-140a51f39ec9/go.mod h1:RWhSQ3F01an8KD00VjzRBZOMcE5eV2Cy0/l4ZkeieyU= github.com/Selly-Modules/mongodb v0.0.0-20211013094205-a8ab24a96c4c h1:1l6QmAl43maG9zFyUXrPQVUjyVt0vy/2Saz992UR+Sc= github.com/Selly-Modules/mongodb v0.0.0-20211013094205-a8ab24a96c4c/go.mod h1:C9O0Bgl9i6szjntMjBdEvaFSqG2UPOgHUspIWIJ93JQ= +github.com/allegro/bigcache/v3 v3.0.1 h1:Q4Xl3chywXuJNOw7NV+MeySd3zGQDj4KCpkCg0te8mc= +github.com/allegro/bigcache/v3 v3.0.1/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -91,6 +93,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= +github.com/thoas/go-funk v0.9.1/go.mod h1:+IWnUfUmFO1+WVYQWQtIJHeRRdaIyyYglZN7xzUPe4Q= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= diff --git a/internal/constant.go b/internal/constant.go index c048f83..1a97219 100644 --- a/internal/constant.go +++ b/internal/constant.go @@ -7,4 +7,6 @@ const ( passwordHashingCost = 14 TablePrefixDefault = "usermngmt" + + RoleTypeAdmin = "admin" ) diff --git a/role/db.go b/role/db.go index 55209e0..9884664 100644 --- a/role/db.go +++ b/role/db.go @@ -7,20 +7,9 @@ import ( "github.com/Selly-Modules/logger" "github.com/Selly-Modules/usermngmt/database" "github.com/Selly-Modules/usermngmt/model" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/options" ) -func findByID(ctx context.Context, id primitive.ObjectID) (model.DBRole, error) { - var ( - doc model.DBRole - col = database.GetRoleCol() - ) - err := col.FindOne(ctx, bson.M{"_id": id}).Decode(&doc) - return doc, err -} - func create(ctx context.Context, doc model.DBRole) error { var ( col = database.GetRoleCol() diff --git a/user/db.go b/user/db.go index d6f9aa4..1c2fccb 100644 --- a/user/db.go +++ b/user/db.go @@ -66,6 +66,21 @@ func roleFindByID(ctx context.Context, id primitive.ObjectID) (model.DBRole, err return doc, err } +// permissionCountByCondition ... +func permissionCountByCondition(ctx context.Context, cond interface{}) int64 { + var ( + col = database.GetPermissionCol() + ) + total, err := col.CountDocuments(ctx, cond) + if err != nil { + logger.Error("usermngmt - Permission - CountDocuments", logger.LogData{ + "err": err.Error(), + "cond": cond, + }) + } + return total +} + func create(ctx context.Context, doc model.DBUser) error { var ( col = database.GetUserCol() diff --git a/user/handle.go b/user/handle.go index fc5831c..23864d4 100644 --- a/user/handle.go +++ b/user/handle.go @@ -7,9 +7,12 @@ import ( "github.com/Selly-Modules/logger" "github.com/Selly-Modules/mongodb" + "github.com/Selly-Modules/usermngmt/cache" "github.com/Selly-Modules/usermngmt/internal" "github.com/Selly-Modules/usermngmt/model" + "github.com/thoas/go-funk" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" ) // Create ... @@ -319,3 +322,55 @@ func LoginWithEmailAndPassword(email, password string) (result model.User, err e result = getResponse(ctx, user) return } + +// HasPermission ... +func HasPermission(userID, permission string) (result bool) { + var ( + ctx = context.Background() + ) + + // Validate userID, permission + if userID == "" || permission == "" { + logger.Error("usermngmt - HasPermission: email or password cannot be empty", logger.LogData{ + "userID": userID, + "permission": permission, + }) + return + } + id, isValid := mongodb.NewIDFromString(userID) + if !isValid { + logger.Error("usermngmt - HasPermission: invalid user id", logger.LogData{ + "userID": userID, + "permission": permission, + }) + return + } + + // Find user + user, _ := findByID(ctx, id) + if user.ID.IsZero() { + logger.Error("usermngmt - HasPermission: user not found", logger.LogData{ + "userID": userID, + "permission": permission, + }) + return + } + + return checkUserHasPermissionFromCache(user.RoleID, permission) +} + +func checkUserHasPermissionFromCache(roleID primitive.ObjectID, permission string) bool { + cachedRole := cache.GetCachedRole(roleID.Hex()) + + // Check permission + if cachedRole.IsAdmin { + return true + } + if _, isValid := funk.FindString(cachedRole.Permissions, func(s string) bool { + return s == permission + }); isValid { + return true + } + + return false +} diff --git a/usermngmt.go b/usermngmt.go index b2a2236..b0617e6 100644 --- a/usermngmt.go +++ b/usermngmt.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/Selly-Modules/mongodb" + "github.com/Selly-Modules/usermngmt/cache" "github.com/Selly-Modules/usermngmt/database" "github.com/Selly-Modules/usermngmt/internal" ) @@ -54,6 +55,9 @@ func Init(config Config) (*Service, error) { return nil, err } + // Init cache + cache.Init() + // Set database database.Set(db, config.TablePrefix)