Skip to content

Commit

Permalink
Merge pull request #948 from signal18/user-gui
Browse files Browse the repository at this point in the history
Implement roles for Users. Auto create user in api-credentials if not exists
  • Loading branch information
svaroqui authored Oct 25, 2024
2 parents 0da231c + 29891ce commit 6fe865a
Show file tree
Hide file tree
Showing 13 changed files with 315 additions and 66 deletions.
2 changes: 2 additions & 0 deletions cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ type Cluster struct {
LogSlack *log.Logger `json:"-"`
JobResults *config.TasksMap `json:"jobResults"`
Grants map[string]string `json:"-"`
Roles map[string]string `json:"-"`
tlog *s18log.TermLog `json:"-"`
htlog *s18log.HttpLog `json:"-"`
SQLGeneralLog s18log.HttpLog `json:"sqlGeneralLog"`
Expand Down Expand Up @@ -371,6 +372,7 @@ func (cluster *Cluster) InitFromConf() {
cluster.DiskType = cluster.Conf.GetDiskType()
cluster.VMType = cluster.Conf.GetVMType()
cluster.Grants = cluster.Conf.GetGrantType()
cluster.Roles = cluster.Conf.GetRoleType()

cluster.QueryRules = make(map[uint32]config.QueryRule)
cluster.Schedule = make(map[string]cron.Entry)
Expand Down
93 changes: 70 additions & 23 deletions cluster/cluster_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ package cluster

import (
"fmt"
"slices"
"strings"

"github.com/signal18/replication-manager/config"
Expand All @@ -21,9 +22,42 @@ type APIUser struct {
Password string `json:"-"`
GitToken string `json:"-"`
GitUser string `json:"-"`
Roles map[string]bool `json:"roles"`
Grants map[string]bool `json:"grants"`
}

func (cluster *Cluster) SetNewUserGrants(u *APIUser, grant string) {
acls := strings.Split(grant, " ")
for key, value := range cluster.Grants {
found := false
for _, acl := range acls {
if strings.HasPrefix(key, acl) && acl != "" {
found = true
break
}
}
u.Grants[value] = found
}
}

func (cluster *Cluster) SetNewUserRoles(u *APIUser, roles string) {
list := strings.Split(roles, " ")

if u.Grants[config.GrantGlobalGrant] && roles == "" {
u.Roles[config.RoleSysOps] = true
u.Roles[config.RoleDBOps] = true
return
}

for key, value := range cluster.Roles {
found := false
if slices.Contains(list, key) {
found = true
}
u.Roles[value] = found
}
}

func (u *APIUser) Granted(grant string) error {
if value, ok := u.Grants[grant]; ok {
if !value {
Expand Down Expand Up @@ -59,19 +93,34 @@ func (cluster *Cluster) GetAPIUser(strUser string, strPassword string) (APIUser,
return APIUser{}, fmt.Errorf("user not found")
}

func (cluster *Cluster) SaveUserAcls(user string) string {
var aEnabledAcls []string
for grant, value := range cluster.APIUsers[user].Grants {
if value {
aEnabledAcls = append(aEnabledAcls, grant)
}
}
return strings.Join(aEnabledAcls, " ")
}

func (cluster *Cluster) SaveUserRoles(user string) string {
var aEnabledRoles []string
for grant, value := range cluster.APIUsers[user].Roles {
if value {
aEnabledRoles = append(aEnabledRoles, grant)
}
}
return strings.Join(aEnabledRoles, " ")
}

func (cluster *Cluster) SaveAcls() {
credentials := strings.Split(cluster.Conf.GetDecryptedValue("api-credentials")+","+cluster.Conf.GetDecryptedValue("api-credentials-external"), ",")
var aUserAcls []string
for _, credential := range credentials {
user, _ := misc.SplitPair(credential)
var aEnabledAcls []string
for grant, value := range cluster.APIUsers[user].Grants {
if value {
aEnabledAcls = append(aEnabledAcls, grant)
}
}
enabledAclsCredential := user + ":" + strings.Join(aEnabledAcls, " ")
aUserAcls = append(aUserAcls, enabledAclsCredential)
enabledAcls := cluster.SaveUserAcls(user)
enabledRoles := cluster.SaveUserRoles(user)
aUserAcls = append(aUserAcls, user+":"+enabledAcls+":"+enabledRoles)
}
cluster.Conf.APIUsersACLAllow = strings.Join(aUserAcls, ",")
cluster.Conf.APIUsersACLDiscard = ""
Expand All @@ -92,28 +141,26 @@ func (cluster *Cluster) LoadAPIUsers() error {
// fmt.Printf(cluster.Conf.Secrets["api-credentials"].Value + "," + cluster.Conf.Secrets["api-credentials-external"].Value)
meUsers := make(map[string]APIUser)
for _, credential := range credentials {
// Assign User Credentials
var newapiuser APIUser

newapiuser.User, newapiuser.Password = misc.SplitPair(credential)
newapiuser.Password = cluster.Conf.GetDecryptedPassword("api-credentials", newapiuser.Password)
usersAllowACL := strings.Split(cluster.Conf.APIUsersACLAllow, ",")
newapiuser.Grants = make(map[string]bool)
newapiuser.Roles = make(map[string]bool)

// Assign Roles and ACLs
usersAllowACL := strings.Split(cluster.Conf.APIUsersACLAllow, ",")
for _, userACL := range usersAllowACL {
useracl, listacls := misc.SplitPair(userACL)
acls := strings.Split(listacls, " ")
if useracl == newapiuser.User {
for key, value := range cluster.Grants {
found := false
for _, acl := range acls {
if strings.HasPrefix(key, acl) && acl != "" {
found = true
break
}
}
newapiuser.Grants[value] = found
}
useracl, listacls, listroles, listcluster := misc.SplitAcls(userACL)
cluster_acls := strings.Split(listcluster, " ")

// For compatibility allow empty cluster list ACL
if useracl == newapiuser.User && (listcluster == "" || slices.Contains(cluster_acls, cluster.Name)) {
cluster.SetNewUserGrants(&newapiuser, listacls)
cluster.SetNewUserRoles(&newapiuser, listroles)
}
}

usersDiscardACL := strings.Split(cluster.Conf.APIUsersACLDiscard, ",")
for _, userACL := range usersDiscardACL {
useracl, listacls := misc.SplitPair(userACL)
Expand Down
29 changes: 8 additions & 21 deletions cluster/cluster_add.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,51 +158,38 @@ func (cluster *Cluster) AddSeededProxy(prx string, srv string, port string, user
type UserForm struct {
Username string `json:"username"`
Password string `json:"password"`
Roles string `json:"roles"`
Grants string `json:"grants"`
}

func (cluster *Cluster) AddUser(userform UserForm) error {
user := userform.Username
roles := userform.Roles
grants := userform.Grants
pass, _ := cluster.GeneratePassword()
if _, ok := cluster.APIUsers[user]; ok {
cluster.LogModulePrintf(cluster.Conf.Verbose, config.ConstLogModGeneral, config.LvlErr, "User %s already exist ", user)
} else {
if cluster.Conf.GetDecryptedValue("api-credentials-external") == "" {
cluster.Conf.APIUsersExternal = user + ":" + pass

cluster.Conf.APIUsersExternal = user + ":" + pass + ":" + roles
} else {
cluster.Conf.APIUsersExternal = cluster.Conf.GetDecryptedValue("api-credentials-external") + "," + user + ":" + pass
cluster.Conf.APIUsersExternal = cluster.Conf.GetDecryptedValue("api-credentials-external") + "," + user + ":" + pass + ":" + roles
}
var new_secret config.Secret
new_secret.Value = cluster.Conf.APIUsersExternal
new_secret.OldValue = cluster.Conf.GetDecryptedValue("api-credentials-external")
cluster.Conf.Secrets["api-credentials-external"] = new_secret

// Assign ACL before reloading
cluster.Conf.APIUsersACLAllow = cluster.Conf.APIUsersACLAllow + "," + user + ":" + grants + ":" + roles + ":" + cluster.Name

cluster.LoadAPIUsers()
cluster.AddUserGrants(user, grants)
cluster.Save()
}

return nil
}

func (cluster *Cluster) AddUserGrants(user string, grants string) {

acls := strings.Split(grants, " ")
for key, value := range cluster.Grants {
found := false
for _, acl := range acls {
if strings.HasPrefix(key, acl) && acl != "" {
found = true
break
}
}
cluster.APIUsers[user].Grants[value] = found
}

cluster.SaveAcls()
}

func (cluster *Cluster) AddShardingHostGroup(proxy *MariadbShardProxy) error {
if cluster.Conf.ClusterHead != "" {
return nil
Expand Down
25 changes: 25 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -878,6 +878,20 @@ type Grant struct {
Enable bool `json:"enable"`
}

type Role struct {
Role string `json:"role"`
Enable bool `json:"enable"`
}

const (
RoleSysOps string = "sysops"
RoleDBOps string = "dbops"
RoleExtSysOps string = "extsysops"
RoleExtDBOps string = "extdbops"
RoleSponsor string = "sponsor"
RoleVisitor string = "visitor"
)

const (
GrantDBStart string = "db-start"
GrantDBStop string = "db-stop"
Expand Down Expand Up @@ -1889,6 +1903,17 @@ func (conf *Config) GetGrantType() map[string]string {
}
}

func (conf *Config) GetRoleType() map[string]string {
return map[string]string{
RoleSysOps: RoleSysOps,
RoleDBOps: RoleDBOps,
RoleExtSysOps: RoleExtSysOps,
RoleExtDBOps: RoleExtDBOps,
RoleSponsor: RoleSponsor,
RoleVisitor: RoleVisitor,
}
}

func (conf *Config) GetDockerRepos(file string, is_not_embed bool) ([]DockerRepo, error) {
var repos DockerRepos
var byteValue []byte
Expand Down
51 changes: 51 additions & 0 deletions server/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import (
"github.com/gorilla/mux"
"github.com/signal18/replication-manager/cert"
"github.com/signal18/replication-manager/cluster"
"github.com/signal18/replication-manager/config"
"github.com/signal18/replication-manager/regtest"
"github.com/signal18/replication-manager/share"
"github.com/signal18/replication-manager/utils/githelper"
Expand Down Expand Up @@ -360,6 +361,31 @@ func (repman *ReplicationManager) IsValidClusterACL(r *http.Request, cluster *cl
return false, ""
}

func (repman *ReplicationManager) GetUserFromRequest(r *http.Request) string {

token, err := request.ParseFromRequest(r, request.AuthorizationHeaderExtractor, func(token *jwt.Token) (interface{}, error) {
vk, _ := jwt.ParseRSAPublicKeyFromPEM(verificationKey)
return vk, nil
})

if err == nil {
claims := token.Claims.(jwt.MapClaims)
userinfo := claims["CustomUserInfo"]
mycutinfo := userinfo.(map[string]interface{})
meuser := mycutinfo["Name"].(string)
_, ok := mycutinfo["profile"]

if ok {
if strings.Contains(mycutinfo["profile"].(string), repman.Conf.OAuthProvider) /*&& strings.Contains(mycutinfo["email_verified"]*/ {
return mycutinfo["email"].(string)
}
}
return meuser
}

return ""
}

func (repman *ReplicationManager) loginHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
var user userCredentials
Expand Down Expand Up @@ -753,8 +779,33 @@ func (repman *ReplicationManager) jsonResponse(apiresponse interface{}, w http.R
func (repman *ReplicationManager) handlerMuxClusterAdd(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
vars := mux.Vars(r)

username := repman.GetUserFromRequest(r)
if username == "" {
http.Error(w, "User is not valid", http.StatusInternalServerError)
return
}

repman.AddCluster(vars["clusterName"], "")
// Create user and grant for new cluster
cl := repman.getClusterByName(vars["clusterName"])
if cl != nil {
if u, ok := cl.APIUsers[username]; !ok {
// Create user and grant for new cluster
userform := cluster.UserForm{
Username: username,
Roles: strings.Join(([]string{config.RoleSponsor, config.RoleDBOps, config.RoleSysOps}), " "),
Grants: "cluster db proxy prov",
}

cl.AddUser(userform)
} else {
// Update grant for new cluster
cl.SetNewUserGrants(&u, "cluster db proxy prov")
cl.SetNewUserRoles(&u, strings.Join(([]string{config.RoleSponsor, config.RoleDBOps, config.RoleSysOps}), " "))
cl.APIUsers[u.User] = u
}
}
}

func (repman *ReplicationManager) handlerMuxClusterDelete(w http.ResponseWriter, r *http.Request) {
Expand Down
22 changes: 20 additions & 2 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ type ReplicationManager struct {
ServicePlans []config.ServicePlan `json:"servicePlans"`
ServiceOrchestrators []config.ConfigVariableType `json:"serviceOrchestrators"`
ServiceAcl []config.Grant `json:"serviceAcl"`
ServiceRoles []config.Role `json:"serviceRoles"`
ServiceRepos []config.DockerRepo `json:"serviceRepos"`
ServiceTarballs []config.Tarball `json:"serviceTarballs"`
ServiceFS map[string]bool `json:"serviceFS"`
Expand Down Expand Up @@ -1755,6 +1756,7 @@ func (repman *ReplicationManager) Run() error {
repman.InitServicePlans()
repman.ServiceOrchestrators = repman.Conf.GetOrchestratorsProv()
repman.InitGrants()
repman.InitRoles()
repman.ServiceRepos, err = repman.Conf.GetDockerRepos(repman.Conf.ShareDir+"/repo/repos.json", repman.Conf.Test)
if err != nil {
repman.Logrus.WithError(err).Errorf("Initialization docker repo failed: %s %s", repman.Conf.ShareDir+"/repo/repos.json", err)
Expand Down Expand Up @@ -2292,10 +2294,14 @@ func (a GrantSorter) Len() int { return len(a) }
func (a GrantSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a GrantSorter) Less(i, j int) bool { return a[i].Grant < a[j].Grant }

func (repman *ReplicationManager) InitGrants() error {
type RoleSorter []config.Role

acls := []config.Grant{}
func (a RoleSorter) Len() int { return len(a) }
func (a RoleSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a RoleSorter) Less(i, j int) bool { return a[i].Role < a[j].Role }

func (repman *ReplicationManager) InitGrants() error {
acls := []config.Grant{}
for _, value := range repman.Conf.GetGrantType() {
var acl config.Grant
acl.Grant = value
Expand All @@ -2306,6 +2312,18 @@ func (repman *ReplicationManager) InitGrants() error {
return nil
}

func (repman *ReplicationManager) InitRoles() error {
roles := []config.Role{}
for _, value := range repman.Conf.GetRoleType() {
var acl config.Role
acl.Role = value
roles = append(roles, acl)
}
repman.ServiceRoles = roles
sort.Sort(RoleSorter(repman.ServiceRoles))
return nil
}

func IsDefault(p string, v *viper.Viper) bool {

return false
Expand Down
Loading

0 comments on commit 6fe865a

Please sign in to comment.