From 4783d79523a724f9a8b1fdcb2c4fdc0510f720fe Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 21 Aug 2026 20:49:15 +0000 Subject: [PATCH] refactor routes, use json encoding for route body parameters, implement modify user/group/pool operations, simplify schema by using comon lib struct tags and RequireAll and AtLeastOne helper functions, properly implement locadb as Backend interface --- app/common/config.go | 7 +- app/common/schema.go | 18 +- app/common/types.go | 7 +- app/common/utils.go | 55 ++++ app/ldap/ldap.go | 42 ++- app/localdb/localdb.go | 130 ++++++++- app/localdb/utils.go | 19 ++ app/main.go | 541 ++++------------------------------- app/operations.go | 120 ++++++-- app/pve/pve.go | 57 +++- app/routes.go | 529 ++++++++++++++++++++++++++++++++++ app/utils.go | 37 --- configs/template.config.json | 3 + 13 files changed, 959 insertions(+), 606 deletions(-) create mode 100644 app/common/utils.go create mode 100644 app/localdb/utils.go create mode 100644 app/routes.go delete mode 100644 app/utils.go diff --git a/app/common/config.go b/app/common/config.go index f9b8000..0ab1ac7 100644 --- a/app/common/config.go +++ b/app/common/config.go @@ -25,6 +25,10 @@ type LDAPConfig struct { Verify bool } +type LocalDBConfig struct { + Path string `json:"path"` +} + type Config struct { ListenPort int `json:"listenPort"` SessionCookieName string `json:"sessionCookieName"` @@ -34,7 +38,8 @@ type Config struct { Secure bool `json:"secure"` MaxAge int `json:"maxAge"` } - PVE PVEConfig `json:"pve"` + PVE PVEConfig `json:"pve"` + LocalDB LocalDBConfig `json:"localdb"` } func GetConfig(configPath string) Config { diff --git a/app/common/schema.go b/app/common/schema.go index 1015a86..c455574 100644 --- a/app/common/schema.go +++ b/app/common/schema.go @@ -1,21 +1,7 @@ package app type Login struct { // login body struct - UsernameRaw string `form:"username" binding:"required"` + UsernameRaw string `json:"username" binding:"required"` Username Username - Password string `form:"password" binding:"required"` -} - -type UserFormRequired struct { // add user body struct - CN string `form:"cn" binding:"required"` - SN string `form:"sn" binding:"required"` - Mail string `form:"mail" binding:"required"` - Password string `form:"password" binding:"required"` -} - -type UserFormOptional struct { // modify user body struct - CN string `form:"cn"` - SN string `form:"sn"` - Mail string `form:"mail"` - Password string `form:"password"` + Password string `json:"password" binding:"required"` } diff --git a/app/common/types.go b/app/common/types.go index 7f04456..e5fd8c6 100644 --- a/app/common/types.go +++ b/app/common/types.go @@ -3,15 +3,18 @@ package app import paas "proxmoxaas-common-lib" type Backend interface { - NewPool(poolname string) (int, error) + NewPool(poolname string, pool Pool) (int, error) + ModPool(poolname string, pool Pool) (int, error) GetPool(poolname string) (Pool, []string, int, error) // []string members DelPool(poolname string) (int, error) - NewGroup(groupname Groupname) (int, error) + NewGroup(groupname Groupname, group Group) (int, error) + ModGroup(groupname Groupname, group Group) (int, error) GetGroup(groupname Groupname) (Group, []string, int, error) // []string members DelGroup(groupname Groupname) (int, error) AddGroupToPool(groupname Groupname, poolname string) (int, error) DelGroupFromPool(groupname Groupname, poolname string) (int, error) NewUser(username Username, user User) (int, error) + ModUser(username Username, user User) (int, error) GetUser(username Username) (User, int, error) DelUser(username Username) (int, error) AddUserToGroup(username Username, groupname Groupname) (int, error) diff --git a/app/common/utils.go b/app/common/utils.go new file mode 100644 index 0000000..97802ad --- /dev/null +++ b/app/common/utils.go @@ -0,0 +1,55 @@ +package app + +import ( + "reflect" +) + +// RequireAll ensures that EVERY non-excluded exported field in the struct is non-zero. +func RequireAll(v any, excludes ...string) bool { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Pointer { + val = val.Elem() + } + typ := val.Type() + + excludeMap := make(map[string]bool) + for _, ex := range excludes { + excludeMap[ex] = true + } + + for i := 0; i < val.NumField(); i++ { + fieldName := typ.Field(i).Name + if excludeMap[fieldName] { + continue + } + if val.Field(i).IsZero() { + return false + } + } + return true +} + +// AtLeastOne ensures that AT LEAST ONE non-excluded exported field is non-zero. +func AtLeastOne(v any, excludes ...string) bool { + val := reflect.ValueOf(v) + if val.Kind() == reflect.Pointer { + val = val.Elem() + } + typ := val.Type() + + excludeMap := make(map[string]bool) + for _, ex := range excludes { + excludeMap[ex] = true + } + + for i := 0; i < val.NumField(); i++ { + fieldName := typ.Field(i).Name + if excludeMap[fieldName] { + continue + } + if !val.Field(i).IsZero() { + return true + } + } + return false +} diff --git a/app/ldap/ldap.go b/app/ldap/ldap.go index 3cc128a..2655a6e 100644 --- a/app/ldap/ldap.go +++ b/app/ldap/ldap.go @@ -78,8 +78,10 @@ func (l LDAPClient) GetUser(username common.Username) (common.User, int, error) ) searchResponse, err := l.client.Search(searchRequest) // perform search - if err != nil { - return user, http.StatusBadRequest, err + if ldap.IsErrorAnyOf(err, ldap.LDAPResultNoSuchObject) { + return user, http.StatusNotFound, err + } else if err != nil { + return user, http.StatusInternalServerError, err } entry := searchResponse.Entries[0] @@ -91,10 +93,10 @@ func (l LDAPClient) GetUser(username common.Username) (common.User, int, error) } func (l LDAPClient) NewUser(username common.Username, user common.User) (int, error) { - if user.CN == "" || user.SN == "" || user.Password == "" || user.Mail == "" { + if !common.RequireAll(user, "Username") { return http.StatusBadRequest, ldap.NewError( ldap.LDAPResultUnwillingToPerform, - errors.New("missing one of required fields: cn, sn, mail, userpassword"), + errors.New("requires all of fields: cn, sn, mail, password"), ) } @@ -117,10 +119,10 @@ func (l LDAPClient) NewUser(username common.Username, user common.User) (int, er } func (l LDAPClient) ModUser(username common.Username, user common.User) (int, error) { - if user.CN == "" && user.SN == "" && user.Password == "" && user.Mail == "" { + if !common.AtLeastOne(user, "Username") { return http.StatusBadRequest, ldap.NewError( ldap.LDAPResultUnwillingToPerform, - errors.New("requires one of fields: cn, sn, mail, userpassword"), + errors.New("requires one of fields: cn, sn, mail, password"), ) } @@ -180,8 +182,10 @@ func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, []string ) searchResponse, err := l.client.Search(searchRequest) // perform search - if err != nil { - return group, members, http.StatusBadRequest, err + if ldap.IsErrorAnyOf(err, ldap.LDAPResultNoSuchObject) { + return group, members, http.StatusNotFound, err + } else if err != nil { + return group, members, http.StatusInternalServerError, err } entry := searchResponse.Entries[0] @@ -195,7 +199,7 @@ func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, []string return group, members, http.StatusOK, nil } -func (l LDAPClient) NewGroup(groupname common.Groupname) (int, error) { +func (l LDAPClient) NewGroup(groupname common.Groupname, group common.Group) (int, error) { // add new group by ID only addRequest := ldap.NewAddRequest( fmt.Sprintf("cn=%s,ou=groups,%s", groupname.GroupID, l.config.BaseDN), // DN @@ -214,19 +218,7 @@ func (l LDAPClient) NewGroup(groupname common.Groupname) (int, error) { } func (l LDAPClient) ModGroup(groupname common.Groupname, group common.Group) (int, error) { - modifyRequest := ldap.NewModifyRequest( - fmt.Sprintf("cn=%s,ou=groups,%s", groupname.GroupID, l.config.BaseDN), - nil, - ) - - modifyRequest.Replace("cn", []string{groupname.GroupID}) - - err := l.client.Modify(modifyRequest) - if err != nil { - return http.StatusBadRequest, err - } - - return http.StatusOK, nil + return http.StatusNotImplemented, fmt.Errorf("ldap does not implement modification of groups") } func (l LDAPClient) DelGroup(groupname common.Groupname) (int, error) { @@ -285,7 +277,11 @@ func (l LDAPClient) DelUserFromGroup(username common.Username, groupname common. return http.StatusOK, nil } -func (l LDAPClient) NewPool(poolname string) (int, error) { +func (l LDAPClient) NewPool(poolname string, pool common.Pool) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") +} + +func (l LDAPClient) ModPool(poolname string, pool common.Pool) (int, error) { return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") } diff --git a/app/localdb/localdb.go b/app/localdb/localdb.go index 9cbc0a7..3627484 100644 --- a/app/localdb/localdb.go +++ b/app/localdb/localdb.go @@ -9,32 +9,38 @@ import ( ) type DB struct { + path string data map[string]common.Pool } -func LoadDB(localDBPath string) (DB, error) { - db := DB{} +var db *DB + +func (db *DB) load(localDBPath string) error { + db.data = make(map[string]common.Pool) + db.path = localDBPath root, err := os.OpenRoot(".") if err != nil { - return db, err + return err } defer root.Close() content, err := root.ReadFile(localDBPath) if err != nil { - return db, err + return err } err = json.Unmarshal(content, &db.data) if err != nil { - return db, err + return err } - return db, nil + return nil } -func SaveDB(localDBPath string, db DB) error { - json, err := json.Marshal(db.data) +func (db *DB) save() error { + localDBPath := db.path + // write to file with pretty print for readability reasons + json, err := json.MarshalIndent(db.data, "", "\t") if err != nil { return err } @@ -42,10 +48,116 @@ func SaveDB(localDBPath string, db DB) error { return err } +func NewClientFromCredentials(config common.LocalDBConfig, username common.Username, password string) (common.Backend, int, error) { + if db != nil { + return *db, http.StatusOK, nil + } else { + // load localdb if this is the first time + db = &DB{} + err := db.load(config.Path) + if err != nil { + return *db, http.StatusInternalServerError, err + } else { + return *db, http.StatusOK, nil + } + } +} + func (localdb DB) GetPool(poolname string) (common.Pool, []string, int, error) { pool, ok := localdb.data[poolname] if !ok { - return pool, []string{}, http.StatusNotFound, fmt.Errorf("pool %s not in localdb", poolname) + return pool, []string{}, http.StatusNotFound, fmt.Errorf("localdb pool %s does not exist", poolname) } return pool, []string{}, http.StatusOK, nil } + +func (localdb DB) NewPool(poolname string, pool common.Pool) (int, error) { + _, ok := localdb.data[poolname] + if ok { + return http.StatusBadRequest, fmt.Errorf("localdb pool %s already exists", poolname) + } + localdb.data[poolname] = pool + err := localdb.save() + if err != nil { + return http.StatusInternalServerError, err + } else { + return http.StatusOK, nil + } +} + +func (localdb DB) ModPool(poolname string, pool common.Pool) (int, error) { + _, ok := localdb.data[poolname] + if !ok { + return http.StatusBadRequest, fmt.Errorf("localdb pool %s does not exist", poolname) + } + old_pool := localdb.data[poolname] + MergeNonZero(&old_pool, &pool) + err := localdb.save() + if err != nil { + return http.StatusInternalServerError, err + } else { + return http.StatusOK, nil + } +} + +func (localdb DB) DelPool(poolname string) (int, error) { + _, ok := localdb.data[poolname] + if !ok { + return http.StatusBadRequest, fmt.Errorf("localdb pool %s does not exist", poolname) + } + delete(localdb.data, poolname) + err := localdb.save() + if err != nil { + return http.StatusInternalServerError, err + } else { + return http.StatusOK, nil + } +} + +func (localdb DB) NewGroup(groupname common.Groupname, group common.Group) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement groups") +} + +func (localdb DB) ModGroup(groupname common.Groupname, group common.Group) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement groups") +} + +func (localdb DB) GetGroup(groupname common.Groupname) (common.Group, []string, int, error) { + return common.Group{}, []string{}, http.StatusNotImplemented, fmt.Errorf("localdb does not implement groups") +} + +func (localdb DB) DelGroup(groupname common.Groupname) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement groups") +} + +func (localdb DB) AddGroupToPool(groupname common.Groupname, poolname string) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement groups") +} + +func (localdb DB) DelGroupFromPool(groupname common.Groupname, poolname string) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement groups") +} + +func (localdb DB) NewUser(username common.Username, user common.User) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement users") +} + +func (localdb DB) ModUser(username common.Username, user common.User) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement users") +} + +func (localdb DB) GetUser(username common.Username) (common.User, int, error) { + return common.User{}, http.StatusNotImplemented, fmt.Errorf("localdb does not implement users") +} + +func (localdb DB) DelUser(username common.Username) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement users") +} + +func (localdb DB) AddUserToGroup(username common.Username, groupname common.Groupname) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement users") +} + +func (localdb DB) DelUserFromGroup(username common.Username, groupname common.Groupname) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("localdb does not implement users") +} diff --git a/app/localdb/utils.go b/app/localdb/utils.go new file mode 100644 index 0000000..1742a3f --- /dev/null +++ b/app/localdb/utils.go @@ -0,0 +1,19 @@ +package localdb + +import "reflect" + +// MergeNonZero overwrites fields in dst with fields in src iff src is not a zero value. +func MergeNonZero[T any](dst *T, src *T) { + vDst := reflect.ValueOf(dst).Elem() + vSrc := reflect.ValueOf(src).Elem() + + for i := 0; i < vDst.NumField(); i++ { + dstField := vDst.Field(i) + srcField := vSrc.Field(i) + + // Only set if the field is exported (CanSet) and the source is not a zero value + if dstField.CanSet() && !srcField.IsZero() { + dstField.Set(srcField) + } + } +} diff --git a/app/main.go b/app/main.go index 881553f..6d58cb8 100644 --- a/app/main.go +++ b/app/main.go @@ -11,18 +11,27 @@ import ( "strconv" common "access-manager-api/app/common" - ldap "access-manager-api/app/ldap" - localdb "access-manager-api/app/localdb" - pve "access-manager-api/app/pve" - paas "proxmoxaas-common-lib" "github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions/cookie" "github.com/gin-gonic/gin" "github.com/luthermonson/go-proxmox" - uuid "github.com/nu7hatch/gouuid" ) +type Realm struct { + Type string + Config any +} + +type UserSession struct { + PVE common.Backend + Realm struct { + Name string + Handler common.Backend + } + DB common.Backend +} + var Version = "1.0.0" var Config common.Config var UserSessions map[string]*UserSession @@ -30,25 +39,19 @@ var Realms map[string]Realm func Run() { configPath := flag.String("config", "config.json", "path to config.json file") - localDBPath := flag.String("localdb", "localdb.json", "path to localdb.json file") flag.Parse() // load config values - var err error Config = common.GetConfig(*configPath) // already exits if failed log.Printf("Read in config from %s\n", *configPath) - // load localdb - db, err := localdb.LoadDB(*localDBPath) - if err != nil { - log.Fatalf("Error when reading localdb file: %s\n", err) - } - log.Printf("Read in localdb from %s\n", *localDBPath) - - // setup router + // setup gin gin.SetMode(gin.ReleaseMode) - router := SetupAPISessionStore(&Config) + router := gin.Default() + + // setup api auth cookies + SetupAPISessionStore(router, &Config) // get realms from proxmox Realms = make(map[string]Realm) @@ -61,486 +64,39 @@ func Run() { c.JSON(http.StatusOK, gin.H{"version": Version}) }) - router.POST("/ticket", func(c *gin.Context) { - body := common.Login{} - err := c.ShouldBind(&body) - if err != nil { // bad request from binding - c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) - return - } - - // attempt to parse username - body.Username, err = paas.ParseUsername(body.UsernameRaw) - if err != nil { // username format incorrect - c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) - return - } - handler := Realms[body.Username.Realm].Type - - userbackends := UserSession{} - - // always bind proxmox backend - PVEClient, code, err := pve.NewClientFromCredentials(Config.PVE, body.Username, body.Password) - if err != nil { // pve client failed to bind - c.JSON(code, gin.H{"auth": false, "error": err.Error()}) - return - } - userbackends.PVE = PVEClient - - // bind backend by type - switch handler { - case "pve": - case "ldap": - config := Realms[body.Username.Realm].Config.(common.LDAPConfig) - LDAPClient, code, err := ldap.NewClientFromCredentials(config, body.Username, body.Password) - if err != nil { // ldap client failed to bind - c.JSON(code, gin.H{"auth": false, "error": err.Error()}) - return - } - userbackends.Realm.Name = body.Username.Realm - userbackends.Realm.Handler = LDAPClient - default: - c.JSON(code, gin.H{"auth": false, "error": fmt.Errorf("user realm %s is not supported", body.Username.Realm)}) - return - } - - userbackends.DB = &db - - // successful binding at this point - // create new session - session := sessions.Default(c) - // create random uuid to map user to backends - uuid, _ := uuid.NewV4() - // set uuid mapping in session - session.Set("SessionUUID", uuid.String()) - // set uuid mapping in LDAPSessions - UserSessions[uuid.String()] = &userbackends - // save the session - err = session.Save() - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"auth": false}) - } else { - // return successful auth - c.JSON(http.StatusOK, gin.H{"auth": true}) - } - }) - - router.DELETE("/ticket", func(c *gin.Context) { - // get session uuid from session cookie - session := sessions.Default(c) - SessionUUID := session.Get("SessionUUID") - if SessionUUID == nil { - c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) - return - } - uuid := SessionUUID.(string) - - // delete uuid entry from user sessions - delete(UserSessions, uuid) // deletes uuid mapping - session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so session cookie is deleted - err := session.Save() // if save somehow fails, it should be ok since the uuid mapping is already deleted - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"auth": false}) - } else { - c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) - } - }) - - router.GET("/pools/:poolid", func(c *gin.Context) { - poolid, ok := c.Params.Get("poolid") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - pool, code, err := GetPool(backends, poolid) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.JSON(http.StatusOK, gin.H{"pool": pool}) - } - }) - - router.POST("/pools/:poolid", func(c *gin.Context) { - poolid, ok := c.Params.Get("poolid") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = NewPool(backends, poolid) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.DELETE("/pools/:poolid", func(c *gin.Context) { - poolid, ok := c.Params.Get("poolid") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = DelPool(backends, poolid) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.GET("/groups/:groupname", func(c *gin.Context) { - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - groupname, err := common.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - group, code, err := GetGroup(backends, groupname) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.JSON(http.StatusOK, gin.H{"group": group}) - } - }) - - router.POST("/groups/:groupname", func(c *gin.Context) { - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - groupname, err := paas.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = NewGroup(backends, groupname) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.DELETE("/groups/:groupname", func(c *gin.Context) { - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - groupname, err := paas.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = DelGroup(backends, groupname) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.POST("/pools/:poolid/groups/:groupname", func(c *gin.Context) { - poolid, ok := c.Params.Get("poolid") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - groupname, err := paas.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = AddGroupToPool(backends, groupname, poolid) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.DELETE("/pools/:poolid/groups/:groupname", func(c *gin.Context) { - poolid, ok := c.Params.Get("poolid") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - groupname, err := paas.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = DelGroupFromPool(backends, groupname, poolid) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.GET("/users/:username", func(c *gin.Context) { - username_str, ok := c.Params.Get("username") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) - return - } - - username, err := common.ParseUsername(username_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - user, code, err := GetUser(backends, username) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.JSON(http.StatusOK, gin.H{"user": user}) - } - }) - - router.POST("/users/:username", func(c *gin.Context) { - username_str, ok := c.Params.Get("username") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - username, err := paas.ParseUsername(username_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - form := common.UserFormRequired{} - err = c.ShouldBind(&form) - if err != nil { // failed binding, usually missing form field - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) - return - } - - user := common.User{} - user.CN = form.CN - user.SN = form.SN - user.Mail = form.Mail - user.Password = form.Password - - code, err = NewUser(backends, username, user) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.DELETE("/users/:username", func(c *gin.Context) { - username_str, ok := c.Params.Get("username") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - username, err := paas.ParseUsername(username_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = DelUser(backends, username) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.POST("/groups/:groupname/users/:username", func(c *gin.Context) { - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - username_str, ok := c.Params.Get("username") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter username")}) - return - } - - groupname, err := paas.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - username, err := paas.ParseUsername(username_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = AddUserToGroup(backends, username, groupname) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) - - router.DELETE("/groups/:groupname/users/:username", func(c *gin.Context) { - groupname_str, ok := c.Params.Get("groupname") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) - return - } - - username_str, ok := c.Params.Get("username") - if !ok { - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter username")}) - return - } - - groupname, err := paas.ParseGroupname(groupname_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - username, err := paas.ParseUsername(username_str) - if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err}) - return - } - - backends, code, err := GetUserSessionFromContext(c) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - return - } - - code, err = DelUserFromGroup(backends, username, groupname) - if err != nil { - c.JSON(code, gin.H{"error": err.Error()}) - } else { - c.Status(http.StatusOK) - } - }) + router.POST("/ticket", POST_Ticket) + router.DELETE("/ticket", DELETE_Ticket) + router.GET("/pools/:poolid", GET_Pool) + router.POST("/pools/:poolid", POST_Pool) + router.DELETE("/pools/:poolid", DELETE_Pool) + router.GET("/groups/:groupname", GET_Group) + router.POST("/groups/:groupname", POST_Group) + router.DELETE("/groups/:groupname", DELETE_Group) + router.POST("/pools/:poolid/groups/:groupname", POST_Pool_Group) + router.DELETE("/pools/:poolid/groups/:groupname") + router.GET("/users/:username", GET_User) + router.POST("/users/:username", POST_User) + router.DELETE("/users/:username", DELETE_User) + router.POST("/groups/:groupname/users/:username", POST_Group_User) + router.DELETE("/groups/:groupname/users/:username", DELETE_Group_User) log.Printf("Starting Access Manager API on port %s\n", strconv.Itoa(Config.ListenPort)) - err = router.Run("0.0.0.0:" + strconv.Itoa(Config.ListenPort)) + err := router.Run("0.0.0.0:" + strconv.Itoa(Config.ListenPort)) if err != nil { log.Fatalf("Error starting router: %s", err.Error()) } } -func SetupAPISessionStore(config *common.Config) *gin.Engine { - secretKey := make([]byte, 256) - n, _ := rand.Read(secretKey) // rand Read never returns an error, always crashes on error - log.Printf("Generated session secret key of length %d\n", n) +func SetupAPISessionStore(router *gin.Engine, config *common.Config) { + authKey := make([]byte, 32) + encrKey := make([]byte, 32) + n, _ := rand.Read(authKey) // rand Read never returns an error, always crashes on error + log.Printf("Generated cookie session authentication key of length %d\n", n) + n, _ = rand.Read(encrKey) // rand Read never returns an error, always crashes on error + log.Printf("Generated cookie session encryption key of length %d\n", n) - router := gin.Default() - store := cookie.NewStore(secretKey) + store := cookie.NewStore(authKey, encrKey) store.Options(sessions.Options{ Path: config.SessionCookie.Path, HttpOnly: config.SessionCookie.HttpOnly, @@ -550,8 +106,17 @@ func SetupAPISessionStore(config *common.Config) *gin.Engine { router.Use(sessions.Sessions(config.SessionCookieName, store)) log.Printf("Started cookie store (Name: %s Params: %+v)\n", config.SessionCookieName, config.SessionCookie) +} - return router +func GetUserSessionFromContext(c *gin.Context) (*UserSession, int, error) { + session := sessions.Default(c) + SessionUUID := session.Get("SessionUUID") + if SessionUUID == nil { + return nil, http.StatusUnauthorized, fmt.Errorf("no auth session found") + } + uuid := SessionUUID.(string) + usersession := UserSessions[uuid] + return usersession, http.StatusOK, nil } func GetRealmsFromPVE(config *common.Config) map[string]Realm { diff --git a/app/operations.go b/app/operations.go index 2f90e14..5998eaa 100644 --- a/app/operations.go +++ b/app/operations.go @@ -3,13 +3,46 @@ package app import ( common "access-manager-api/app/common" "access-manager-api/app/ldap" + proxmox "access-manager-api/app/pve" "fmt" "net/http" ) -func NewPool(backends *UserSession, poolname string) (int, error) { - // only pve backend handles pools - return backends.PVE.NewPool(poolname) +func NewPool(backends *UserSession, poolname string, pool common.Pool) (int, error) { + code, err := backends.PVE.NewPool(poolname, pool) + if err != nil { + return code, err + } + + code, err = backends.DB.NewPool(poolname, pool) + if err != nil { + // try to undo pve add pool operation + backends.PVE.DelPool(poolname) + return code, err + } + + return http.StatusOK, nil +} + +func ModPool(backends *UserSession, poolname string, pool common.Pool) (int, error) { + oldpool, code, err := GetPool(backends, poolname) + if err != nil { + return code, err + } + + code, err = backends.PVE.ModPool(poolname, pool) + if err != nil { + return code, err + } + + code, err = backends.DB.ModPool(poolname, pool) + if err != nil { + // try to undo pve mod pool operation + backends.PVE.ModPool(poolname, oldpool) + return code, err + } + + return http.StatusOK, nil } // get pool recursive resolving groups @@ -25,7 +58,8 @@ func GetPool(backends *UserSession, poolname string) (common.Pool, int, error) { if err != nil { return pool, code, err } - // assign pool id from PVE, assign everything else from DB + + // assign pool id and pool members from PVE, assign everything else from DB pool.PoolID = pvepool.PoolID pool.Resources = dbpool.Resources pool.AllowedNodes = dbpool.AllowedNodes @@ -43,6 +77,7 @@ func GetPool(backends *UserSession, poolname string) (common.Pool, int, error) { if err != nil { return pool, code, err } + // members already are filtered by PAASClientRole group.Role = Config.PVE.PAASClientRole pool.Groups = append(pool.Groups, group) } @@ -50,20 +85,42 @@ func GetPool(backends *UserSession, poolname string) (common.Pool, int, error) { return pool, http.StatusOK, nil } func DelPool(backends *UserSession, poolname string) (int, error) { - // only pve backend handles pools - return backends.PVE.DelPool(poolname) + codepve, errpve := backends.PVE.DelPool(poolname) + + codedb, errdb := backends.DB.DelPool(poolname) + + if errpve != nil || errdb != nil { + return http.StatusInternalServerError, fmt.Errorf("error deleting pool: (pve:%d, %s) (db:%d, %s)", codepve, errpve, codedb, errdb) + } else { + return http.StatusOK, nil + } } -func NewGroup(backends *UserSession, groupname common.Groupname) (int, error) { +func NewGroup(backends *UserSession, groupname common.Groupname, group common.Group) (int, error) { if groupname.Realm == "pve" { - return backends.PVE.NewGroup(groupname) + return backends.PVE.NewGroup(groupname, group) } else if groupname.Realm == backends.Realm.Name { - realm_handler := backends.Realm.Handler.(common.Backend) - code, err := realm_handler.NewGroup(groupname) + realm_handler := backends.Realm.Handler + code, err := realm_handler.NewGroup(groupname, group) if err != nil { return code, err } - return backends.PVE.SyncRealms() + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested group") + } +} + +func ModGroup(backends *UserSession, groupname common.Groupname, group common.Group) (int, error) { + if groupname.Realm == "pve" { + return backends.PVE.ModGroup(groupname, group) + } else if groupname.Realm == backends.Realm.Name { + realm_handler := backends.Realm.Handler + code, err := realm_handler.ModGroup(groupname, group) + if err != nil { + return code, err + } + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() } else { return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested group") } @@ -92,7 +149,7 @@ func GetGroup(backends *UserSession, groupname common.Groupname) (common.Group, return group, http.StatusOK, nil } else if groupname.Realm == backends.Realm.Name { - group, members, code, err := backends.Realm.Handler.(common.Backend).GetGroup(groupname) + group, members, code, err := backends.Realm.Handler.GetGroup(groupname) if err != nil { return common.Group{}, code, err } @@ -127,12 +184,12 @@ func DelGroup(backends *UserSession, groupname common.Groupname) (int, error) { if groupname.Realm == "pve" { return backends.PVE.DelGroup(groupname) } else if groupname.Realm == backends.Realm.Name { - realm_handler := backends.Realm.Handler.(common.Backend) + realm_handler := backends.Realm.Handler code, err := realm_handler.DelGroup(groupname) if err != nil { return code, err } - return backends.PVE.SyncRealms() + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() } else { return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested group") } @@ -152,12 +209,29 @@ func NewUser(backends *UserSession, username common.Username, user common.User) if username.Realm == "pve" { return backends.PVE.NewUser(username, user) } else if username.Realm == backends.Realm.Name { - realm_handler := backends.Realm.Handler.(common.Backend) + realm_handler := backends.Realm.Handler code, err := realm_handler.NewUser(username, user) if err != nil { return code, err } - return backends.PVE.SyncRealms() + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested user") + } +} + +func ModUser(backends *UserSession, username common.Username, user common.User) (int, error) { + if username.Realm == "pve" { + return backends.PVE.ModUser(username, user) + } else if username.Realm == backends.Realm.Name { + realm_handler := backends.Realm.Handler + code, err := realm_handler.ModUser(username, user) + if err != nil { + return code, err + } + // todo, most users will not have access to sync realms, but should be able to modify their own user + // will probably use priviledge escalation to give priviledge for modify user operations + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() } else { return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested user") } @@ -172,7 +246,7 @@ func GetUser(backends *UserSession, username common.Username) (common.User, int, } return pveuser, http.StatusOK, nil } else if username.Realm == backends.Realm.Name { - realmuser, code, err := backends.Realm.Handler.(common.Backend).GetUser(username) + realmuser, code, err := backends.Realm.Handler.GetUser(username) if err != nil { return common.User{}, code, err } @@ -186,12 +260,12 @@ func DelUser(backends *UserSession, username common.Username) (int, error) { if username.Realm == "pve" { return backends.PVE.DelUser(username) } else if username.Realm == backends.Realm.Name { - realm_handler := backends.Realm.Handler.(common.Backend) + realm_handler := backends.Realm.Handler code, err := realm_handler.DelUser(username) if err != nil { return code, err } - return backends.PVE.SyncRealms() + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() } else { return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested user") } @@ -207,12 +281,12 @@ func AddUserToGroup(backends *UserSession, username common.Username, groupname c // in the future support may be removed return backends.PVE.AddUserToGroup(username, groupname) } else if username.Realm == backends.Realm.Name && groupname.Realm == backends.Realm.Name { // both req user and req group are in realm - realm_handler := backends.Realm.Handler.(common.Backend) + realm_handler := backends.Realm.Handler code, err := realm_handler.AddUserToGroup(username, groupname) if err != nil { return code, err } - return backends.PVE.SyncRealms() + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() } else { // req user in proxmox and req group in realm (not possible to do) return http.StatusUnauthorized, fmt.Errorf("cannot add %s to %s", username.ToString(), groupname.ToString()) } @@ -228,12 +302,12 @@ func DelUserFromGroup(backends *UserSession, username common.Username, groupname // in the future support may be removed return backends.PVE.DelUserFromGroup(username, groupname) } else if username.Realm == backends.Realm.Name && groupname.Realm == backends.Realm.Name { // both req user and req group are in realm - realm_handler := backends.Realm.Handler.(common.Backend) + realm_handler := backends.Realm.Handler code, err := realm_handler.DelUserFromGroup(username, groupname) if err != nil { return code, err } - return backends.PVE.SyncRealms() + return backends.PVE.(proxmox.ProxmoxClient).SyncRealms() } else { // req user in proxmox and req group in realm (not possible to do) return http.StatusUnauthorized, fmt.Errorf("cannot delete %s from %s", username.ToString(), groupname.ToString()) } diff --git a/app/pve/pve.go b/app/pve/pve.go index 1167136..101d166 100644 --- a/app/pve/pve.go +++ b/app/pve/pve.go @@ -21,13 +21,13 @@ type ProxmoxClient struct { func IsProxmoxNotFound(err error) bool { if err != nil { // for whatever reason proxmox returns 500 for user/group/pool not found - return proxmox.IsNotFound(err) || strings.Contains(err.Error(), "no such user") || strings.Contains(err.Error(), "does not exist") + return proxmox.IsNotFound(err) || strings.Contains(err.Error(), "no such") || strings.Contains(err.Error(), "does not exist") } return false } // creates a new client binding with associated permissions -func NewClientFromCredentials(config common.PVEConfig, username common.Username, password string) (*ProxmoxClient, int, error) { +func NewClientFromCredentials(config common.PVEConfig, username common.Username, password string) (common.Backend, int, error) { HTTPClient := http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{}, @@ -47,7 +47,7 @@ func NewClientFromCredentials(config common.PVEConfig, username common.Username, return nil, http.StatusUnauthorized, err } - return &ProxmoxClient{config: &config, client: client}, http.StatusOK, nil + return ProxmoxClient{config: &config, client: client}, http.StatusOK, nil } func (pve ProxmoxClient) SyncRealms() (int, error) { @@ -60,7 +60,7 @@ func (pve ProxmoxClient) SyncRealms() (int, error) { for _, domain := range domains { if domain.Type != "pam" && domain.Type != "pve" { // pam and pve are not external realm types that require sync e := proxmox.IntOrBool(true) - r := string("acl;entry;properties") + r := "acl;entry;properties" err := domain.Sync(context.Background(), proxmox.DomainSyncOptions{ DryRun: false, // we want to make modifications EnableNew: &e, // allow new users and groups @@ -77,7 +77,7 @@ func (pve ProxmoxClient) SyncRealms() (int, error) { return http.StatusOK, nil } -func (pve ProxmoxClient) NewPool(poolname string) (int, error) { +func (pve ProxmoxClient) NewPool(poolname string, pool common.Pool) (int, error) { err := pve.client.NewPool(context.Background(), poolname, "") if proxmox.IsNotAuthorized(err) { return http.StatusUnauthorized, err @@ -88,6 +88,11 @@ func (pve ProxmoxClient) NewPool(poolname string) (int, error) { } } +func (pve ProxmoxClient) ModPool(poolname string, pool common.Pool) (int, error) { + // no-op + return http.StatusOK, nil +} + func (pve ProxmoxClient) GetPool(poolname string) (common.Pool, []string, int, error) { pool := common.Pool{} members := []string{} @@ -138,8 +143,8 @@ func (pve ProxmoxClient) DelPool(poolname string) (int, error) { } } -func (pve ProxmoxClient) NewGroup(groupname common.Groupname) (int, error) { - // add new group ny ID only +func (pve ProxmoxClient) NewGroup(groupname common.Groupname, group common.Group) (int, error) { + // add new group by ID only err := pve.client.NewGroup(context.Background(), groupname.GroupID, "") if proxmox.IsNotAuthorized(err) { return http.StatusUnauthorized, err @@ -150,6 +155,11 @@ func (pve ProxmoxClient) NewGroup(groupname common.Groupname) (int, error) { } } +func (pve ProxmoxClient) ModGroup(groupname common.Groupname, group common.Group) (int, error) { + // no-op + return http.StatusOK, nil +} + func (pve ProxmoxClient) GetGroup(groupname common.Groupname) (common.Group, []string, int, error) { group := common.Group{} members := []string{} @@ -224,6 +234,10 @@ func (pve ProxmoxClient) DelGroupFromPool(groupname common.Groupname, poolname s } func (pve ProxmoxClient) NewUser(username common.Username, user common.User) (int, error) { + if !common.RequireAll(user, "Username") { + return http.StatusBadRequest, fmt.Errorf("missing one of required fields: cn, sn, mail, userpassword") + } + err := pve.client.NewUser(context.Background(), &proxmox.NewUser{ UserID: username.ToString(), Firstname: user.CN, @@ -240,6 +254,35 @@ func (pve ProxmoxClient) NewUser(username common.Username, user common.User) (in } } +func (pve ProxmoxClient) ModUser(username common.Username, user common.User) (int, error) { + if !common.AtLeastOne(user, "Username") { + return http.StatusBadRequest, fmt.Errorf("requires one of fields: cn, sn, mail, userpassword") + } + + pveuser, err := pve.client.User(context.Background(), username.ToString()) + if proxmox.IsNotAuthorized(err) { + return http.StatusUnauthorized, err + } else if IsProxmoxNotFound(err) { + return http.StatusNotFound, err + } else if err != nil { + return http.StatusInternalServerError, err + } + + err = pveuser.Update(context.Background(), proxmox.UserOptions{ + Firstname: user.CN, + Lastname: user.SN, + Email: user.Mail, + // todo userpassword (its a separate pve endpoint) + }) + if proxmox.IsNotAuthorized(err) { + return http.StatusUnauthorized, err + } else if err != nil { + return http.StatusInternalServerError, err + } else { + return http.StatusOK, nil + } +} + func (pve ProxmoxClient) GetUser(username common.Username) (common.User, int, error) { user := common.User{} pveuser, err := pve.client.User(context.Background(), username.ToString()) diff --git a/app/routes.go b/app/routes.go new file mode 100644 index 0000000..d9c7389 --- /dev/null +++ b/app/routes.go @@ -0,0 +1,529 @@ +package app + +import ( + common "access-manager-api/app/common" + ldap "access-manager-api/app/ldap" + "access-manager-api/app/localdb" + pve "access-manager-api/app/pve" + "fmt" + "net/http" + paas "proxmoxaas-common-lib" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + uuid "github.com/nu7hatch/gouuid" +) + +func POST_Ticket(c *gin.Context) { + body := common.Login{} + err := c.ShouldBindJSON(&body) + if err != nil { // bad request from binding + c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) + return + } + + // attempt to parse username + body.Username, err = paas.ParseUsername(body.UsernameRaw) + if err != nil { // username format incorrect + c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) + return + } + handler := Realms[body.Username.Realm].Type + + userbackends := UserSession{} + + // always bind proxmox backend + PVEClient, code, err := pve.NewClientFromCredentials(Config.PVE, body.Username, body.Password) + if err != nil { // pve client failed to bind + c.JSON(code, gin.H{"auth": false, "error": err.Error()}) + return + } + userbackends.PVE = PVEClient + + // bind backend by type + switch handler { + case "pve": + case "ldap": + config := Realms[body.Username.Realm].Config.(common.LDAPConfig) + LDAPClient, code, err := ldap.NewClientFromCredentials(config, body.Username, body.Password) + if err != nil { // ldap client failed to bind + c.JSON(code, gin.H{"auth": false, "error": err.Error()}) + return + } + userbackends.Realm.Name = body.Username.Realm + userbackends.Realm.Handler = LDAPClient + default: + c.JSON(code, gin.H{"auth": false, "error": fmt.Errorf("user realm %s is not supported", body.Username.Realm)}) + return + } + + // failing to load lcoaldb is a fatal error + userbackends.DB, code, err = localdb.NewClientFromCredentials(Config.LocalDB, body.Username, body.Password) + if err != nil { + c.JSON(code, gin.H{"auth": false, "error": err.Error()}) + return + } + + // successful binding at this point + // create new session + session := sessions.Default(c) + // create random uuid to map user to backends + uuid, _ := uuid.NewV4() + // set uuid mapping in session + session.Set("SessionUUID", uuid.String()) + // set uuid mapping in LDAPSessions + UserSessions[uuid.String()] = &userbackends + // save the session + err = session.Save() + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"auth": false, "error": err.Error()}) + } else { + // return successful auth + c.JSON(http.StatusOK, gin.H{"auth": true}) + } +} + +func DELETE_Ticket(c *gin.Context) { + // get session uuid from session cookie + session := sessions.Default(c) + SessionUUID := session.Get("SessionUUID") + if SessionUUID == nil { + c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) + return + } + uuid := SessionUUID.(string) + + // delete uuid entry from user sessions + delete(UserSessions, uuid) // deletes uuid mapping + session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so session cookie is deleted + err := session.Save() // if save somehow fails, it should be ok since the uuid mapping is already deleted + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"auth": false}) + } else { + c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) + } +} + +func GET_Pool(c *gin.Context) { + poolid, ok := c.Params.Get("poolid") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + pool, code, err := GetPool(backends, poolid) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.JSON(http.StatusOK, gin.H{"pool": pool}) + } +} + +func POST_Pool(c *gin.Context) { + poolid, ok := c.Params.Get("poolid") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + // read request body + pool := common.Pool{} + err = c.ShouldBindJSON(&pool) + if err != nil { // failed binding, usually missing user field + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + _, code, _ = GetPool(backends, poolid) // test pool existence + + if code == http.StatusNotFound { // pool does not already exist, create new pool + code, err = NewPool(backends, poolid, pool) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } + } else { + code, err = ModPool(backends, poolid, pool) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } + } +} + +func DELETE_Pool(c *gin.Context) { + poolid, ok := c.Params.Get("poolid") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = DelPool(backends, poolid) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} + +func GET_Group(c *gin.Context) { + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + groupname, err := common.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + group, code, err := GetGroup(backends, groupname) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.JSON(http.StatusOK, gin.H{"group": group}) + } +} + +func POST_Group(c *gin.Context) { + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + groupname, err := paas.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + // read request body + group := common.Group{} + err = c.ShouldBindJSON(&group) + if err != nil { // failed binding, usually missing user field + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + _, code, _ = GetGroup(backends, groupname) // test group existence + + if code == http.StatusNotFound { // group does not already exist, create new group + code, err = NewGroup(backends, groupname, group) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } + } else { + code, err = ModGroup(backends, groupname, group) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } + } +} + +func DELETE_Group(c *gin.Context) { + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + groupname, err := paas.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = DelGroup(backends, groupname) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} + +func POST_Pool_Group(c *gin.Context) { + poolid, ok := c.Params.Get("poolid") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + groupname, err := paas.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = AddGroupToPool(backends, groupname, poolid) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} + +func DELETE_Pool_Group(c *gin.Context) { + poolid, ok := c.Params.Get("poolid") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + groupname, err := paas.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = DelGroupFromPool(backends, groupname, poolid) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} + +func GET_User(c *gin.Context) { + username_str, ok := c.Params.Get("username") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")}) + return + } + + username, err := common.ParseUsername(username_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + user, code, err := GetUser(backends, username) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.JSON(http.StatusOK, gin.H{"user": user}) + } +} + +func POST_User(c *gin.Context) { + username_str, ok := c.Params.Get("username") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + username, err := paas.ParseUsername(username_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + // read request body + user := common.User{} + err = c.ShouldBindJSON(&user) + if err != nil { // failed binding, usually missing user field + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + _, code, _ = GetUser(backends, username) // test user existence + + if code == http.StatusNotFound { // user does not already exist, create new user + code, err = NewUser(backends, username, user) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } + } else { + code, err = ModUser(backends, username, user) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } + } +} + +func DELETE_User(c *gin.Context) { + username_str, ok := c.Params.Get("username") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + username, err := paas.ParseUsername(username_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = DelUser(backends, username) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} + +func POST_Group_User(c *gin.Context) { + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + username_str, ok := c.Params.Get("username") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter username")}) + return + } + + groupname, err := paas.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + username, err := paas.ParseUsername(username_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = AddUserToGroup(backends, username, groupname) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} + +func DELETE_Group_User(c *gin.Context) { + groupname_str, ok := c.Params.Get("groupname") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")}) + return + } + + username_str, ok := c.Params.Get("username") + if !ok { + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter username")}) + return + } + + groupname, err := paas.ParseGroupname(groupname_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + username, err := paas.ParseUsername(username_str) + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return + } + + backends, code, err := GetUserSessionFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + return + } + + code, err = DelUserFromGroup(backends, username, groupname) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(http.StatusOK) + } +} diff --git a/app/utils.go b/app/utils.go deleted file mode 100644 index 987083f..0000000 --- a/app/utils.go +++ /dev/null @@ -1,37 +0,0 @@ -package app - -import ( - "fmt" - "net/http" - - "github.com/gin-contrib/sessions" - "github.com/gin-gonic/gin" - - localdb "access-manager-api/app/localdb" - pve "access-manager-api/app/pve" -) - -type Realm struct { - Type string - Config any -} - -type UserSession struct { - PVE *pve.ProxmoxClient - Realm struct { - Name string - Handler any - } - DB *localdb.DB -} - -func GetUserSessionFromContext(c *gin.Context) (*UserSession, int, error) { - session := sessions.Default(c) - SessionUUID := session.Get("SessionUUID") - if SessionUUID == nil { - return nil, http.StatusUnauthorized, fmt.Errorf("no auth session found") - } - uuid := SessionUUID.(string) - usersession := UserSessions[uuid] - return usersession, http.StatusOK, nil -} diff --git a/configs/template.config.json b/configs/template.config.json index f0d5cdb..5eb25c3 100644 --- a/configs/template.config.json +++ b/configs/template.config.json @@ -16,5 +16,8 @@ "uuid": "" }, "paas-client-role": "" + }, + "localdb": { + "path": "localdb.json" } } \ No newline at end of file