From ea0ae4ffe191704ae4132990ffe0d12a5f63feb9 Mon Sep 17 00:00:00 2001 From: yxxhero <11087727+yxxhero@users.noreply.github.com> Date: Mon, 2 Aug 2021 20:14:39 +0800 Subject: [PATCH] Add rbac support (#474) Signed-off-by: yxxhero Co-authored-by: Gaius --- deploy/charts/dragonfly/README.md | 2 +- go.mod | 6 +- go.sum | 26 ++++- manager/database/database.go | 29 +++++- manager/handlers/permission.go | 125 ++++++++++++++++++++++++ manager/middlewares/jwt.go | 28 ++++-- manager/middlewares/rbac.go | 48 +++++++++ manager/permission/rbac/rbac.go | 141 +++++++++++++++++++++++++++ manager/permission/rbac/rbac_test.go | 92 +++++++++++++++++ manager/server/router.go | 22 ++++- manager/server/server.go | 9 +- manager/service/permission.go | 86 ++++++++++++++++ manager/service/service.go | 24 +++-- manager/types/permission.go | 19 ++++ 14 files changed, 628 insertions(+), 29 deletions(-) create mode 100644 manager/handlers/permission.go create mode 100644 manager/middlewares/rbac.go create mode 100644 manager/permission/rbac/rbac.go create mode 100644 manager/permission/rbac/rbac_test.go create mode 100644 manager/service/permission.go create mode 100644 manager/types/permission.go diff --git a/deploy/charts/dragonfly/README.md b/deploy/charts/dragonfly/README.md index 689715f7b3e..a65293a265e 100644 --- a/deploy/charts/dragonfly/README.md +++ b/deploy/charts/dragonfly/README.md @@ -33,4 +33,4 @@ helm install --namespace dragonfly-system dragonfly https://... ## Reference -[/~https://github.com/openkruise/kruise/blob/master/charts/kruise/v0.9.0/README.md](/~https://github.com/openkruise/kruise/blob/master/charts/kruise/v0.9.0/README.md) +[/~https://github.com/dragonflyoss/Dragonfly2/blob/main/deploy/charts/dragonfly/README.md](/~https://github.com/dragonflyoss/Dragonfly2/blob/main/deploy/charts/dragonfly/README.md) diff --git a/go.mod b/go.mod index 68cf5cddd01..af7a1f45d91 100644 --- a/go.mod +++ b/go.mod @@ -9,11 +9,13 @@ require ( github.com/aliyun/aliyun-oss-go-sdk v2.1.6+incompatible github.com/appleboy/gin-jwt/v2 v2.6.4 github.com/baiyubin/aliyun-sts-go-sdk v0.0.0-20180326062324-cfa1a18b161f // indirect + github.com/casbin/casbin/v2 v2.34.1 + github.com/casbin/gorm-adapter/v3 v3.3.2 github.com/colinmarc/hdfs/v2 v2.2.0 github.com/docker/go-units v0.4.0 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v0.6.1 - github.com/gin-gonic/gin v1.7.0 + github.com/gin-gonic/gin v1.7.2 github.com/go-echarts/statsview v0.3.4 github.com/go-http-utils/headers v0.0.0-20181008091004-fed159eddc2a github.com/go-redis/cache/v8 v8.4.1 @@ -70,7 +72,7 @@ require ( gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 gorm.io/datatypes v1.0.1 gorm.io/driver/mysql v1.0.5 - gorm.io/gorm v1.21.6 + gorm.io/gorm v1.21.9 honnef.co/go/tools v0.0.1-2020.1.3 // indirect k8s.io/apimachinery v0.20.6 // indirect k8s.io/client-go v11.0.0+incompatible diff --git a/go.sum b/go.sum index 16f357cbd93..62c26a994c3 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/HuKeping/rbtree v0.0.0-20210106022122-8ad34838eb2b h1:zDhQxG7rm8RLgLgi6NpfaVFsop+zxw5hwhbzHr624us= github.com/HuKeping/rbtree v0.0.0-20210106022122-8ad34838eb2b/go.mod h1:bODsl3NElqKlgf1UkBLj67fYmY5DsqkKrrYm/kMT/6Y= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible h1:1G1pk05UrOh0NlF1oeaaix1x8XzrfjIDK47TY0Zehcw= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= @@ -50,6 +52,11 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/casbin/casbin/v2 v2.28.3/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/casbin/v2 v2.34.1 h1:LpxduJ9/A+0hbTD84PWbR0fSSTcZBjM+mcPU0rZcv8E= +github.com/casbin/casbin/v2 v2.34.1/go.mod h1:vByNa/Fchek0KZUgG5wEsl7iFsiviAYKRtgrQfcJqHg= +github.com/casbin/gorm-adapter/v3 v3.3.2 h1:DPbDD63KOlyvtmXfxQiS+3sbkA46i6thOvXqsBzf8KY= +github.com/casbin/gorm-adapter/v3 v3.3.2/go.mod h1:z0J/CpAznL6MyXMPnzltbLBI6lykprGc1rusy+vMJps= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -74,6 +81,7 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/denisenkom/go-mssqldb v0.0.0-20200428022330-06a60b6afbbc/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.9.0 h1:RSohk2RsiZqLZ0zCjtfn3S4Gp4exhpBWHyQ7D0yGjAk= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= @@ -107,8 +115,8 @@ github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeME github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.7.0 h1:jGB9xAJQ12AIGNB4HguylppmDK1Am9ppF7XnGXXJuoU= -github.com/gin-gonic/gin v1.7.0/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= +github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA= +github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY= github.com/go-echarts/go-echarts/v2 v2.2.3 h1:H8oPdUpzuiV2K8S4xYZa1JRNjP3U0h7HVqvhPrmCk1A= github.com/go-echarts/go-echarts/v2 v2.2.3/go.mod h1:6TOomEztzGDVDkOSCFBq3ed7xOYfbOqhaBzD0YV771A= github.com/go-echarts/statsview v0.3.4 h1:CCuytRAutdnF901NrR4BzSjHXjUp8OyA3/iopgG/1/Y= @@ -171,6 +179,7 @@ github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4er github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -364,8 +373,9 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.8.0 h1:9xohqzkUwzR4Ga4ivdTcawVS89YSDVxXMa3xJX3cGzg= +github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lyft/protoc-gen-star v0.5.1/go.mod h1:9toiA3cC7z5uVbODF7kEQ91Xn7XNFkVUl+SrEe+ZORU= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -887,20 +897,28 @@ gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclp gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/datatypes v1.0.1 h1:6npnXbBtjpSb7FFVA2dG/llyTN8tvZfbUqs+WyLrYgQ= gorm.io/datatypes v1.0.1/go.mod h1:HEHoUU3/PO5ZXfAJcVWl11+zWlE16+O0X2DgJEb4Ixs= +gorm.io/driver/mysql v1.0.3/go.mod h1:twGxftLBlFgNVNakL7F+P/x9oYqoymG3YYT8cAfI9oI= gorm.io/driver/mysql v1.0.5 h1:WAAmvLK2rG0tCOqrf5XcLi2QUwugd4rcVJ/W3aoon9o= gorm.io/driver/mysql v1.0.5/go.mod h1:N1OIhHAIhx5SunkMGqWbGFVeh4yTNWKmMo1GOAsohLI= gorm.io/driver/postgres v1.0.8 h1:PAgM+PaHOSAeroTjHkCHCBIHHoBIf9RgPWGo8dF2DA8= gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= +gorm.io/driver/sqlserver v1.0.4/go.mod h1:ciEo5btfITTBCj9BkoUVDvgQbUdLWQNqdFY5OGuGnRg= gorm.io/driver/sqlserver v1.0.7 h1:uwUtb0kdFwW5PkRbd2KJ2h4wlsqvLSjox1XVg/RnzRE= gorm.io/driver/sqlserver v1.0.7/go.mod h1:ng66aHI47ZIKz/vvnxzDoonzmTS8HXP+JYlgg67wOog= +gorm.io/gorm v1.20.0/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.20.11/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.3/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= -gorm.io/gorm v1.21.6 h1:xEFbH7WShsnAM+HeRNv7lOeyqmDAK+dDnf1AMf/cVPQ= gorm.io/gorm v1.21.6/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/gorm v1.21.9 h1:INieZtn4P2Pw6xPJ8MzT0G4WUOsHq3RhfuDF1M6GW0E= +gorm.io/gorm v1.21.9/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= +gorm.io/plugin/dbresolver v1.1.0 h1:cegr4DeprR6SkLIQlKhJLYxH8muFbJ4SmnojXvoeb00= +gorm.io/plugin/dbresolver v1.1.0/go.mod h1:tpImigFAEejCALOttyhWqsy4vfa2Uh/vAUVnL5IRF7Y= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/manager/database/database.go b/manager/database/database.go index 486b1a998cd..245dd1165af 100644 --- a/manager/database/database.go +++ b/manager/database/database.go @@ -6,6 +6,7 @@ import ( "d7y.io/dragonfly/v2/manager/config" "d7y.io/dragonfly/v2/manager/model" "github.com/go-redis/redis/v8" + "golang.org/x/crypto/bcrypt" "gorm.io/driver/mysql" "gorm.io/gorm" "gorm.io/gorm/schema" @@ -74,7 +75,9 @@ func migrate(db *gorm.DB) error { func seed(db *gorm.DB) error { var cdnClusterCount int64 - db.Model(model.CDNCluster{}).Count(&cdnClusterCount) + if err := db.Model(model.CDNCluster{}).Count(&cdnClusterCount).Error; err != nil { + return err + } if cdnClusterCount <= 0 { if err := db.Create(&model.CDNCluster{ Name: "cdn-cluster-1", @@ -84,8 +87,30 @@ func seed(db *gorm.DB) error { } } + var adminUserCount int64 + var adminUserName = "admin" + if err := db.Model(model.User{}).Where("name = ?", adminUserName).Count(&adminUserCount).Error; err != nil { + return err + } + if adminUserCount <= 0 { + encryptedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte("Dragonfly2"), bcrypt.MinCost) + if err != nil { + return err + } + if err := db.Create(&model.User{ + EncryptedPassword: string(encryptedPasswordBytes), + Name: adminUserName, + Email: fmt.Sprintf("%s@Dragonfly2.com", adminUserName), + State: model.UserStateEnabled, + }).Error; err != nil { + return err + } + } + var schedulerClusterCount int64 - db.Model(model.SchedulerCluster{}).Count(&schedulerClusterCount) + if err := db.Model(model.SchedulerCluster{}).Count(&schedulerClusterCount).Error; err != nil { + return err + } if schedulerClusterCount <= 0 { if err := db.Create(&model.SchedulerCluster{ Name: "scheduler-cluster-1", diff --git a/manager/handlers/permission.go b/manager/handlers/permission.go new file mode 100644 index 00000000000..aa16b8ec873 --- /dev/null +++ b/manager/handlers/permission.go @@ -0,0 +1,125 @@ +package handlers + +import ( + "net/http" + + "d7y.io/dragonfly/v2/manager/types" + "github.com/gin-gonic/gin" +) + +// @Summary Get PermissionGroups +// @Description Get PermissionGroups +// @Tags permission +// @Produce json +// @Success 200 {object} RoutesInfo +// @Failure 400 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /permission/groups [get] + +func (h *Handlers) GetPermissionGroups(g *gin.Engine) func(ctx *gin.Context) { + return func(ctx *gin.Context) { + + permissionGroups := h.Service.GetPermissionGroups(g) + + ctx.JSON(http.StatusOK, permissionGroups) + } +} + +// @Summary Get User Roles +// @Description Get User Roles +// @Tags permission +// @Produce json +// @Success 200 {object} RoutesInfo +// @Failure 400 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /permission/roles/{subject} [get] + +func (h *Handlers) GetRolesForUser(ctx *gin.Context) { + var params types.UserRolesParams + if err := ctx.ShouldBindUri(¶ms); err != nil { + ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()}) + return + } + roles, err := h.Service.GetRolesForUser(params.Subject) + if err != nil { + ctx.Error(err) + return + } + ctx.JSON(http.StatusOK, gin.H{"roles": roles}) + +} + +// @Summary Judge User Role +// @Description Judge User Role +// @Tags permission +// @Produce json +// @Success 200 {object} +// @Failure 400 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /permission/{subject}/{object}/{action} [get] + +func (h *Handlers) HasRoleForUser(ctx *gin.Context) { + var params types.UserHasRoleParams + if err := ctx.ShouldBindUri(¶ms); err != nil { + ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()}) + return + } + if params.Subject == "admin" { + ctx.JSON(http.StatusOK, gin.H{"has": true}) + return + } + has, err := h.Service.HasRoleForUser(params.Subject, params.Object, params.Action) + if err != nil { + ctx.Error(err) + return + } + ctx.JSON(http.StatusOK, gin.H{"has": has}) +} + +// @Summary Create Permission +// @Description Create Permission by json config +// @Tags permission +// @Accept json +// @Produce json +// @Success 200 +// @Failure 400 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /permission [post] + +func (h *Handlers) CreatePermission(ctx *gin.Context) { + var json types.PolicyRequest + if err := ctx.ShouldBindJSON(&json); err != nil { + ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()}) + return + } + err := h.Service.CreatePermission(json) + if err != nil { + ctx.Error(err) + return + } + ctx.Status(http.StatusOK) +} + +// @Summary Destroy Permission +// @Description Destroy Permission by json config +// @Tags permission +// @Accept json +// @Produce json +// @Success 200 +// @Failure 400 {object} HTTPError +// @Failure 500 {object} HTTPError +// @Router /permission [delete] + +func (h *Handlers) DestroyPermission(ctx *gin.Context) { + var json types.PolicyRequest + if err := ctx.ShouldBindJSON(&json); err != nil { + ctx.JSON(http.StatusUnprocessableEntity, gin.H{"errors": err.Error()}) + return + } + err := h.Service.DestroyPermission(json) + if err != nil { + ctx.Error(err) + return + } + ctx.Status(http.StatusOK) +} diff --git a/manager/middlewares/jwt.go b/manager/middlewares/jwt.go index e0df6bfa4a4..d9625f16ae8 100644 --- a/manager/middlewares/jwt.go +++ b/manager/middlewares/jwt.go @@ -1,8 +1,10 @@ package middlewares import ( + "net/http" "time" + "d7y.io/dragonfly/v2/manager/model" "d7y.io/dragonfly/v2/manager/service" "d7y.io/dragonfly/v2/manager/types" jwt "github.com/appleboy/gin-jwt/v2" @@ -10,11 +12,11 @@ import ( ) type user struct { - ID uint + userName string } func Jwt(service service.REST) (*jwt.GinJWTMiddleware, error) { - var identityKey = "id" + var identityKey = "username" authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{ Realm: "Dragonfly", Key: []byte("Secret Key"), @@ -24,9 +26,19 @@ func Jwt(service service.REST) (*jwt.GinJWTMiddleware, error) { IdentityHandler: func(c *gin.Context) interface{} { claims := jwt.ExtractClaims(c) - return &user{ - ID: claims[identityKey].(uint), + userNmae, ok := claims[identityKey] + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{ + "message": "Unavailable token: require username info", + }) + c.Abort() + return nil } + u := &user{ + userName: userNmae.(string), + } + c.Set("userName", u.userName) + return u }, Authenticator: func(c *gin.Context) (interface{}, error) { @@ -40,15 +52,13 @@ func Jwt(service service.REST) (*jwt.GinJWTMiddleware, error) { return "", jwt.ErrFailedAuthentication } - return &user{ - ID: u.ID, - }, nil + return u, nil }, PayloadFunc: func(data interface{}) jwt.MapClaims { - if u, ok := data.(user); ok { + if u, ok := data.(*model.User); ok { return jwt.MapClaims{ - identityKey: u.ID, + identityKey: u.Name, } } return jwt.MapClaims{} diff --git a/manager/middlewares/rbac.go b/manager/middlewares/rbac.go new file mode 100644 index 00000000000..3a627216383 --- /dev/null +++ b/manager/middlewares/rbac.go @@ -0,0 +1,48 @@ +package middlewares + +import ( + "net/http" + + "d7y.io/dragonfly/v2/manager/permission/rbac" + "github.com/casbin/casbin/v2" + "github.com/gin-gonic/gin" +) + +func RBAC(e *casbin.Enforcer) gin.HandlerFunc { + return func(c *gin.Context) { + userName := c.GetString("userName") + // request path + p := c.Request.URL.Path + permissionGroupName, err := rbac.GetAPIGroupName(p) + if err != nil { + c.Next() + return + } + // request method + m := c.Request.Method + action := rbac.HTTPMethodToAction(m) + // rbac validation + adminRes, err := e.HasRoleForUser(userName, "admin") + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{ + "message": "permission validate error", + }) + c.Abort() + return + } + if adminRes { + c.Next() + return + } + res, err := e.Enforce(userName, permissionGroupName, action) + if err != nil || !res { + c.JSON(http.StatusUnauthorized, gin.H{ + "message": "permission validate error", + }) + c.Abort() + return + } + c.Next() + + } +} diff --git a/manager/permission/rbac/rbac.go b/manager/permission/rbac/rbac.go new file mode 100644 index 00000000000..c8d366c09d2 --- /dev/null +++ b/manager/permission/rbac/rbac.go @@ -0,0 +1,141 @@ +package rbac + +import ( + "errors" + "net/http" + "regexp" + "strings" + + logger "d7y.io/dragonfly/v2/internal/dflog" + "d7y.io/dragonfly/v2/pkg/util/stringutils" + "github.com/casbin/casbin/v2" + "github.com/casbin/casbin/v2/model" + gormadapter "github.com/casbin/gorm-adapter/v3" + "github.com/gin-gonic/gin" + "gorm.io/gorm" +) + +// Syntax for models see https://casbin.org/docs/en/syntax-for-models +const modelText = ` +[request_definition] +r = sub, obj, act + +[policy_definition] +p = sub, obj, act + +[role_definition] +g = _, _ + +[policy_effect] +e = some(where (p.eft == allow)) + +[matchers] +m = g(r.sub, p.sub) && r.obj == p.obj && (r.act == p.act || p.act == "*") || r.sub == "admin" +` + +func NewEnforcer(gdb *gorm.DB) (*casbin.Enforcer, error) { + gormAdapter, err := gormadapter.NewAdapterByDB(gdb) + if err != nil { + return nil, err + } + m, err := model.NewModelFromString(modelText) + if err != nil { + return nil, err + } + enforcer, err := casbin.NewEnforcer(m, gormAdapter) + if err != nil { + return nil, err + } + return enforcer, nil +} + +func InitRole(e *casbin.Enforcer, g *gin.Engine) error { + systemRoles := SystemRoles(g) + + for _, role := range systemRoles { + roleInfo := strings.Split(role, ":") + _, err := e.AddPolicy(role, roleInfo[0], roleInfo[1]) + if err != nil { + return err + } + } + logger.Info("init and check role success") + return nil + +} + +func GetAPIGroupName(path string) (string, error) { + apiGroupRegexp := regexp.MustCompile(`^/api/v[0-9]+/(?P[\-_a-zA-Z]+)`) + matchs := apiGroupRegexp.FindStringSubmatch(path) + if matchs == nil { + return "", errors.New("faild to find api group") + } + apiGroupName := "" + regexGroupNames := apiGroupRegexp.SubexpNames() + for i, name := range regexGroupNames { + if i != 0 && name == "apiGroup" { + apiGroupName = matchs[i] + } + } + + if apiGroupName != "" { + return apiGroupName, nil + } + return "", errors.New("faild to find api group") + +} + +func RoleName(object, action string) string { + if object == "admin" { + return "admin" + } + roleName := "" + switch action { + case "read": + roleName = object + ":" + "read" + case "write": + roleName = object + ":" + "*" + } + return roleName +} + +func GetAPIGroupNames(g *gin.Engine) []string { + APIGroups := []string{} + for _, route := range g.Routes() { + apiGroupName, err := GetAPIGroupName(route.Path) + if err != nil { + continue + } + if !stringutils.Contains(APIGroups, apiGroupName) { + APIGroups = append(APIGroups, apiGroupName) + } + + } + return APIGroups + +} + +func SystemRoles(g *gin.Engine) []string { + Roles := []string{} + policyKeys := []string{"read", "*"} + + for _, apiGroup := range GetAPIGroupNames(g) { + for _, p := range policyKeys { + if !stringutils.Contains(Roles, apiGroup+":"+p) { + Roles = append(Roles, apiGroup+":"+p) + } + + } + } + return Roles +} + +func HTTPMethodToAction(method string) string { + action := "read" + + if method == http.MethodDelete || method == http.MethodPatch || method == http.MethodPut || method == http.MethodPost { + action = "*" + } + + return action +} diff --git a/manager/permission/rbac/rbac_test.go b/manager/permission/rbac/rbac_test.go new file mode 100644 index 00000000000..0c6a177b0ff --- /dev/null +++ b/manager/permission/rbac/rbac_test.go @@ -0,0 +1,92 @@ +package rbac + +import ( + "testing" +) + +func TestGetApiGroupName(t *testing.T) { + tests := []struct { + path string + exceptedGroupName string + hasError bool + }{ + { + path: "/api/v1/users", + exceptedGroupName: "users", + hasError: false, + }, + { + path: "/api/user", + exceptedGroupName: "", + hasError: true, + }, + } + + for _, tt := range tests { + groupName, err := GetAPIGroupName(tt.path) + if tt.hasError { + if err == nil { + t.Errorf("GetApiGroupName(%s) should return error", tt.path) + } + } + + if groupName != tt.exceptedGroupName { + t.Errorf("GetApiGroupName(%v) = %v, want %v", tt.path, groupName, tt.exceptedGroupName) + } + } + +} + +func TestRoleName(t *testing.T) { + tests := []struct { + object string + action string + exceptedRoleName string + }{ + { + object: "users", + action: "read", + exceptedRoleName: "users:read", + }, + { + object: "cdns", + action: "write", + exceptedRoleName: "cdns:*", + }, + } + + for _, tt := range tests { + roleName := RoleName(tt.object, tt.action) + if roleName != tt.exceptedRoleName { + t.Errorf("RoleName(%v, %v) = %v, want %v", tt.object, tt.action, roleName, tt.exceptedRoleName) + } + } + +} +func TestHTTPMethodToAction(t *testing.T) { + tests := []struct { + method string + exceptedAction string + }{ + { + method: "GET", + exceptedAction: "read", + }, + { + method: "POST", + exceptedAction: "*", + }, + { + method: "UNKNOWN", + exceptedAction: "read", + }, + } + + for _, tt := range tests { + action := HTTPMethodToAction(tt.method) + if action != tt.exceptedAction { + t.Errorf("HttpMethodToAction(%v) = %v, want %v", tt.method, action, tt.exceptedAction) + } + } + +} diff --git a/manager/server/router.go b/manager/server/router.go index 3be46c417f8..45cf2571132 100644 --- a/manager/server/router.go +++ b/manager/server/router.go @@ -3,12 +3,14 @@ package server import ( "d7y.io/dragonfly/v2/manager/handlers" "d7y.io/dragonfly/v2/manager/middlewares" + rbacbase "d7y.io/dragonfly/v2/manager/permission/rbac" "d7y.io/dragonfly/v2/manager/service" + "github.com/casbin/casbin/v2" "github.com/gin-gonic/gin" ginprometheus "github.com/mcuadros/go-gin-prometheus" ) -func initRouter(verbose bool, service service.REST) (*gin.Engine, error) { +func initRouter(verbose bool, service service.REST, enforcer *casbin.Enforcer) (*gin.Engine, error) { // Set mode if !verbose { gin.SetMode(gin.ReleaseMode) @@ -26,6 +28,7 @@ func initRouter(verbose bool, service service.REST) (*gin.Engine, error) { r.Use(gin.Recovery()) r.Use(middlewares.Error()) + rbac := middlewares.RBAC(enforcer) jwt, err := middlewares.Jwt(service) if err != nil { return nil, err @@ -39,7 +42,7 @@ func initRouter(verbose bool, service service.REST) (*gin.Engine, error) { ai.POST("/signin", jwt.LoginHandler) ai.POST("/signout", jwt.LogoutHandler) ai.POST("/refresh_token", jwt.RefreshHandler) - ai.POST("/signup", h.SignUp) + ai.POST("/signup", jwt.MiddlewareFunc(), rbac, h.SignUp) // Scheduler Cluster sc := apiv1.Group("/scheduler-clusters") @@ -76,6 +79,14 @@ func initRouter(verbose bool, service service.REST) (*gin.Engine, error) { ci.GET(":id", h.GetCDN) ci.GET("", h.GetCDNs) + // Permission + pn := apiv1.Group("/permission", jwt.MiddlewareFunc(), rbac) + pn.POST("", h.CreatePermission) + pn.DELETE("", h.DestroyPermission) + pn.GET("/groups", h.GetPermissionGroups(r)) + pn.GET("/roles/:subject", h.GetRolesForUser) + pn.GET("/:subject/:object/:action", h.HasRoleForUser) + // Security Group sg := apiv1.Group("/security-groups") sg.POST("", h.CreateSecurityGroup) @@ -88,5 +99,12 @@ func initRouter(verbose bool, service service.REST) (*gin.Engine, error) { // Health Check r.GET("/healthy/*action", h.GetHealth) + + // Auto init roles and check roles + err = rbacbase.InitRole(enforcer, r) + if err != nil { + return nil, err + } + return r, nil } diff --git a/manager/server/server.go b/manager/server/server.go index 0aaea5515fe..6eb1d5d7f0a 100644 --- a/manager/server/server.go +++ b/manager/server/server.go @@ -24,6 +24,7 @@ import ( "d7y.io/dragonfly/v2/manager/cache" "d7y.io/dragonfly/v2/manager/config" "d7y.io/dragonfly/v2/manager/database" + "d7y.io/dragonfly/v2/manager/permission/rbac" "d7y.io/dragonfly/v2/manager/searcher" "d7y.io/dragonfly/v2/manager/service" "d7y.io/dragonfly/v2/pkg/rpc" @@ -49,6 +50,10 @@ func New(cfg *config.Config) (*Server, error) { if err != nil { return nil, err } + enforcer, err := rbac.NewEnforcer(db.DB) + if err != nil { + return nil, err + } // Initialize cache cache := cache.New(cfg) @@ -57,13 +62,13 @@ func New(cfg *config.Config) (*Server, error) { searcher := searcher.New() // Initialize REST service - restService := service.NewREST(db, cache) + restService := service.NewREST(db, cache, enforcer) // Initialize GRPC service grpcService := service.NewGRPC(db, cache, searcher) // Initialize router - router, err := initRouter(cfg.Verbose, restService) + router, err := initRouter(cfg.Verbose, restService, enforcer) if err != nil { return nil, err } diff --git a/manager/service/permission.go b/manager/service/permission.go new file mode 100644 index 00000000000..2dc061aab51 --- /dev/null +++ b/manager/service/permission.go @@ -0,0 +1,86 @@ +package service + +import ( + "fmt" + "strings" + + logger "d7y.io/dragonfly/v2/internal/dflog" + "d7y.io/dragonfly/v2/manager/permission/rbac" + "d7y.io/dragonfly/v2/manager/types" + "d7y.io/dragonfly/v2/pkg/util/stringutils" + "github.com/gin-gonic/gin" +) + +func (s *rest) GetPermissionGroups(g *gin.Engine) types.PermissionGroups { + groups := rbac.GetAPIGroupNames(g) + if !stringutils.Contains(groups, "admin") { + groups = append(groups, "admin") + } + return groups +} + +func (s *rest) CreatePermission(json types.PolicyRequest) error { + roleName := rbac.RoleName(json.Object, json.Action) + res, err := s.enforcer.AddRoleForUser(json.Subject, roleName) + if err != nil { + return err + } + if !res { + logger.Infof("The role %s of %s already exist. skip!", roleName, json.Subject) + } + return nil +} + +func (s *rest) GetRolesForUser(subject string) ([]map[string]string, error) { + result := []map[string]string{} + policyToAction := map[string]string{ + "read": "read", + "*": "write", + } + res, err := s.enforcer.GetRolesForUser(subject) + if err != nil { + return nil, err + } + + for _, role := range res { + if role == "admin" { + result = append(result, map[string]string{"object": "admin", "description": "admin", "action": ""}) + } else { + roleInfo := strings.Split(role, ":") + action := policyToAction[roleInfo[1]] + result = append(result, map[string]string{"object": roleInfo[0], "description": fmt.Sprintf("%s for %s", action, roleInfo[0]), "action": action}) + } + } + + return result, nil +} + +func (s *rest) HasRoleForUser(subject, object, action string) (bool, error) { + roleName := rbac.RoleName(object, action) + res, err := s.enforcer.HasRoleForUser(subject, roleName) + if err != nil { + return false, err + } + if action == "read" { + writeRoleName := rbac.RoleName(object, "write") + writeRes, err := s.enforcer.HasRoleForUser(subject, writeRoleName) + if err != nil { + return false, err + } + return res || writeRes, nil + + } + return res, nil +} + +func (s *rest) DestroyPermission(json types.PolicyRequest) error { + roleName := rbac.RoleName(json.Object, json.Action) + res, err := s.enforcer.DeleteRoleForUser(json.Subject, roleName) + if err != nil { + return err + } + if !res { + logger.Infof("The role %s of %s already remove. skip!", roleName, json.Subject) + } + return nil +} diff --git a/manager/service/service.go b/manager/service/service.go index d7225d1d924..a69fa755781 100644 --- a/manager/service/service.go +++ b/manager/service/service.go @@ -5,6 +5,8 @@ import ( "d7y.io/dragonfly/v2/manager/database" "d7y.io/dragonfly/v2/manager/model" "d7y.io/dragonfly/v2/manager/types" + "github.com/casbin/casbin/v2" + "github.com/gin-gonic/gin" "github.com/go-redis/redis/v8" "gorm.io/gorm" ) @@ -56,19 +58,27 @@ type REST interface { SignIn(json types.SignInRequest) (*model.User, error) SignUp(json types.SignUpRequest) (*model.User, error) + + GetPermissionGroups(g *gin.Engine) types.PermissionGroups + CreatePermission(json types.PolicyRequest) error + DestroyPermission(json types.PolicyRequest) error + GetRolesForUser(subject string) ([]map[string]string, error) + HasRoleForUser(subject, object, action string) (bool, error) } type rest struct { - db *gorm.DB - rdb *redis.Client - cache *cache.Cache + db *gorm.DB + rdb *redis.Client + cache *cache.Cache + enforcer *casbin.Enforcer } // NewREST returns a new REST instence -func NewREST(database *database.Database, cache *cache.Cache) REST { +func NewREST(database *database.Database, cache *cache.Cache, enforcer *casbin.Enforcer) REST { return &rest{ - db: database.DB, - rdb: database.RDB, - cache: cache, + db: database.DB, + rdb: database.RDB, + cache: cache, + enforcer: enforcer, } } diff --git a/manager/types/permission.go b/manager/types/permission.go new file mode 100644 index 00000000000..ff35a4e21cb --- /dev/null +++ b/manager/types/permission.go @@ -0,0 +1,19 @@ +package types + +type PolicyRequest struct { + Subject string `form:"subject" binding:"required,min=1"` + Object string `form:"object" binding:"required,min=1"` + Action string `form:"aciton" binding:"omitempty,oneof=read write"` +} + +type PermissionGroups []string + +type UserRolesParams struct { + Subject string `uri:"subject" binding:"required"` +} + +type UserHasRoleParams struct { + UserRolesParams + Object string `uri:"object" binding:"required"` + Action string `uri:"action" binding:"omitempty,oneof=read write"` +}