diff --git a/app/common/config.go b/app/common/config.go new file mode 100644 index 0000000..e9fe41f --- /dev/null +++ b/app/common/config.go @@ -0,0 +1,47 @@ +package app + +import ( + "encoding/json" + "os" +) + +type LDAPConfig struct { + LdapURL string `json:"ldapURL"` + StartTLS bool `json:"startTLS"` + BaseDN string `json:"baseDN"` +} + +type PVEConfig struct { + URL string `json:"url"` +} + +type RealmConfig struct { + Handler string `json:"handler"` +} + +type Config struct { + ListenPort int `json:"listenPort"` + SessionCookieName string `json:"sessionCookieName"` + SessionCookie struct { + Path string `json:"path"` + HttpOnly bool `json:"httpOnly"` + Secure bool `json:"secure"` + MaxAge int `json:"maxAge"` + } + LDAP LDAPConfig `json:"ldap"` + PVE PVEConfig `json:"pve"` + Realms map[string]RealmConfig `json:"realms"` +} + +func GetConfig(configPath string) (Config, error) { + content, err := os.ReadFile(configPath) + if err != nil { + return Config{}, err + } + var config Config + err = json.Unmarshal(content, &config) + if err != nil { + return Config{}, err + } + return config, nil +} diff --git a/app/common/schema.go b/app/common/schema.go new file mode 100644 index 0000000..b495d78 --- /dev/null +++ b/app/common/schema.go @@ -0,0 +1,7 @@ +package app + +type Login struct { // login body struct + UsernameRaw string `form:"username" binding:"required"` + Username Username + Password string `form:"password" binding:"required"` +} diff --git a/app/common/types.go b/app/common/types.go new file mode 100644 index 0000000..ba2d4d2 --- /dev/null +++ b/app/common/types.go @@ -0,0 +1,111 @@ +package app + +type BackendClient interface { + BindUser(username string, password string) error + + //GetAllUsers() ([]User, int, error) + GetUser(username string) (User, int, error) + AddUser(username string, user User) (int, error) + ModUser(username string, user User) (int, error) + DelUser(username string) (int, error) + + //GetAllGroups() ([]Group, int, error) + GetGroup(groupname string) (Group, int, error) + AddGroup(groupname string, group Group) (int, error) + ModGroup(groupname string, group Group) (int, error) + DelGroup(groupname string) (int, error) + + AddUserToGroup(username string, groupname string) + RemoveUserFromGroup(username string, groupname string) +} + +type Pool struct { + PoolID string `json:"poolid"` + Path string `json:"-"` // typically /pool/poolid from proxmox, only used internally + Groups []Group `json:"groups"` + Resources map[string]any `json:"resources"` + Cluster Cluster `json:"cluster"` + Templates Templates `json:"templates"` +} + +type Groupname struct { // proxmox typically formats as gid-realm for non pve realms + GroupID string `json:"gid"` + Realm string `json:"realm"` +} + +type Group struct { + Groupname Groupname `json:"groupname"` + Handler string `json:"-"` + Role string `json:"role"` + Users []User `json:"users"` +} + +type Username struct { // ie userid@realm + UserID string `json:"uid"` + Realm string `json:"realm"` +} + +type User struct { + Username Username `json:"username"` + Handler string `json:"-"` + CN string `json:"cn"` // aka first name + SN string `json:"sn"` // aka last name + Mail string `json:"mail"` + Password string `json:"password"` // only used for POST requests +} + +type Cluster struct { + Nodes map[string]bool `json:"nodes"` + VMID VMID `json:"vmid"` + //Pools map[string]bool `json:"pools"` + Backups Backups `json:"backups"` +} + +type VMID struct { + Min int `json:"min"` + MAx int `json:"max"` +} + +type Backups struct { + Max int `json:"max"` +} + +type Templates struct { + Instances struct { + LXC map[string]ResourceTemplate `json:"lxc"` + QEMU map[string]ResourceTemplate `json:"qemu"` + } `json:"instances"` +} + +type SimpleResource struct { + Limits struct { + Global SimpleLimit `json:"global"` + Nodes map[string]SimpleLimit `json:"nodes"` + } `json:"limits"` +} + +type SimpleLimit struct { + Max int `json:"max"` +} + +type MatchResource struct { + Limits struct { + Global []MatchLimit `json:"global"` + Nodes map[string][]MatchLimit `json:"nodes"` + } `json:"limits"` +} + +type MatchLimit struct { + Match string `json:"match"` + Name string `json:"name"` + Max int `json:"max"` +} + +type ResourceTemplate struct { + Value string `json:"value"` + Resource struct { + Enabled bool `json:"enabled"` + Name string `json:"name"` + Amount int `json:"amount"` + } `json:"resource"` +} diff --git a/app/common/utils.go b/app/common/utils.go new file mode 100644 index 0000000..b52bf29 --- /dev/null +++ b/app/common/utils.go @@ -0,0 +1,42 @@ +package app + +import ( + "fmt" + "strings" +) + +func ParseGroupname(groupname string) (Groupname, error) { + g := Groupname{} + x := strings.Split(groupname, "-") + if len(x) == 1 { + g.GroupID = groupname + g.Realm = "pve" + return g, nil + } else if len(x) == 2 { + g.GroupID = x[0] + g.Realm = x[1] + return g, nil + } else { + return g, fmt.Errorf("groupid did not follow the format or -") + } +} + +func ParseUsername(username string) (Username, error) { + u := Username{} + x := strings.Split(username, "@") + if len(x) == 2 { + u.UserID = x[0] + u.Realm = x[1] + return u, nil + } else { + return u, fmt.Errorf("userid did not follow the format @") + } +} + +func (g Groupname) ToString() string { + return fmt.Sprintf("%s-%s", g.GroupID, g.Realm) +} + +func (u Username) ToString() string { + return fmt.Sprintf("%s-%s", u.UserID, u.Realm) +} diff --git a/app/ldap/ldap.go b/app/ldap/ldap.go new file mode 100644 index 0000000..f60ca89 --- /dev/null +++ b/app/ldap/ldap.go @@ -0,0 +1,262 @@ +package ldap + +import ( + "crypto/tls" + "errors" + "fmt" + "net/http" + + "github.com/go-ldap/ldap/v3" + + common "user-manager-api/app/common" +) + +// LDAPClient wrapper struct containing the connection, baseDN, peopleDN, and groupsDN +type LDAPClient struct { + client *ldap.Conn + basedn string + peopledn string + groupsdn string +} + +// returns a new LDAPClient from the config +func NewClientFromCredentials(config common.LDAPConfig, username common.Username, password string) (*LDAPClient, int, error) { + LDAPConn, err := ldap.DialURL(config.LdapURL) + if err != nil { + return nil, http.StatusInternalServerError, err + } + + if config.StartTLS { + err = LDAPConn.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + return nil, http.StatusInternalServerError, err + } + } + + ldap := LDAPClient{ + client: LDAPConn, + basedn: config.BaseDN, + peopledn: "ou=people," + config.BaseDN, + groupsdn: "ou=groups," + config.BaseDN, + } + + userdn := fmt.Sprintf("uid=%s,%s", username.UserID, ldap.peopledn) + err = ldap.client.Bind(userdn, password) + + if err != nil { + return nil, http.StatusUnauthorized, err + } else { + return &ldap, http.StatusOK, nil + } +} + +func (l LDAPClient) GetUser(username common.Username) (common.User, int, error) { + user := common.User{} + + searchRequest := ldap.NewSearchRequest( // setup search for user by uid + fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn), // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + "(&(objectClass=inetOrgPerson))", // The filter to apply + []string{"dn", "cn", "sn", "mail", "uid", "memberOf"}, // A list attributes to retrieve + nil, + ) + + searchResponse, err := l.client.Search(searchRequest) // perform search + if err != nil { + return user, http.StatusBadRequest, err + } + + entry := searchResponse.Entries[0] + + user = LDAPEntryToUser(entry) + + return user, http.StatusOK, nil +} + +func (l LDAPClient) NewUser(username common.Username, user common.User) (int, error) { + if user.CN == "" || user.SN == "" || user.Password == "" || user.Mail == "" { + return http.StatusBadRequest, ldap.NewError( + ldap.LDAPResultUnwillingToPerform, + errors.New("missing one of required fields: cn, sn, mail, userpassword"), + ) + } + + addRequest := ldap.NewAddRequest( + fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn), // DN + nil, // controls + ) + addRequest.Attribute("sn", []string{user.SN}) + addRequest.Attribute("cn", []string{user.CN}) + addRequest.Attribute("mail", []string{user.Mail}) + addRequest.Attribute("userPassword", []string{user.Password}) + addRequest.Attribute("objectClass", []string{"inetOrgPerson"}) + + err := l.client.Add(addRequest) + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) ModUser(username common.Username, user common.User) (int, error) { + if user.CN == "" && user.SN == "" && user.Password == "" && user.Mail == "" { + return http.StatusBadRequest, ldap.NewError( + ldap.LDAPResultUnwillingToPerform, + errors.New("requires one of fields: cn, sn, mail, userpassword"), + ) + } + + modifyRequest := ldap.NewModifyRequest( + fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn), + nil, + ) + if user.CN != "" { + modifyRequest.Replace("cn", []string{user.CN}) + } + if user.SN != "" { + modifyRequest.Replace("sn", []string{user.SN}) + } + if user.Mail != "" { + modifyRequest.Replace("mail", []string{user.Mail}) + } + if user.Password != "" { + modifyRequest.Replace("userPassword", []string{user.Password}) + } + + err := l.client.Modify(modifyRequest) + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) DelUser(username common.Username) (int, error) { + userDN := fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn) + + // assumes that olcMemberOfRefint=true updates member attributes of referenced groups + + deleteUserRequest := ldap.NewDelRequest( // setup delete request + userDN, + nil, + ) + + err := l.client.Del(deleteUserRequest) // delete user + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, int, error) { + group := common.Group{} + + searchRequest := ldap.NewSearchRequest( // setup search for user by uid + fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn), // The base dn to search + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + "(&(objectClass=groupOfNames))", // The filter to apply + []string{"cn", "member"}, // A list attributes to retrieve + nil, + ) + + searchResponse, err := l.client.Search(searchRequest) // perform search + if err != nil { + return group, http.StatusBadRequest, err + } + + entry := searchResponse.Entries[0] + group = LDAPEntryToGroup(entry) + + return group, http.StatusOK, nil +} + +func (l LDAPClient) NewGroup(groupname common.Groupname) (int, error) { + addRequest := ldap.NewAddRequest( + fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn), // DN + nil, // controls + ) + addRequest.Attribute("cn", []string{groupname.GroupID}) + addRequest.Attribute("member", []string{""}) + addRequest.Attribute("objectClass", []string{"groupOfNames"}) + + err := l.client.Add(addRequest) + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) ModGroup(groupname common.Groupname, group common.Group) (int, error) { + modifyRequest := ldap.NewModifyRequest( + fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn), + nil, + ) + + modifyRequest.Replace("cn", []string{groupname.GroupID}) + + err := l.client.Modify(modifyRequest) + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) DelGroup(groupname common.Groupname) (int, error) { + groupDN := fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn) + + // assumes that memberOf overlay will automatically update referenced memberOf attributes + + deleteGroupRequest := ldap.NewDelRequest( // setup delete request + groupDN, + nil, + ) + + err := l.client.Del(deleteGroupRequest) // delete group + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) AddUserToGroup(username common.Username, groupname common.Groupname) (int, error) { + userDN := fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn) + groupDN := fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn) + + modifyRequest := ldap.NewModifyRequest( // modify group member value + groupDN, + nil, + ) + + modifyRequest.Add("member", []string{userDN}) // add user to group member attribute + + err := l.client.Modify(modifyRequest) // modify group + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} + +func (l LDAPClient) DelUserFromGroup(username common.Username, groupname common.Groupname) (int, error) { + userDN := fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn) + groupDN := fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn) + + modifyRequest := ldap.NewModifyRequest( // modify group member value + groupDN, + nil, + ) + + modifyRequest.Delete("member", []string{userDN}) // remove user from group member attribute + + err := l.client.Modify(modifyRequest) // modify group + if err != nil { + return http.StatusBadRequest, err + } + + return http.StatusOK, nil +} diff --git a/app/ldap/utils.go b/app/ldap/utils.go new file mode 100644 index 0000000..f92370c --- /dev/null +++ b/app/ldap/utils.go @@ -0,0 +1,39 @@ +package ldap + +import ( + "github.com/gin-gonic/gin" + "github.com/go-ldap/ldap/v3" + + common "user-manager-api/app/common" +) + +func LDAPEntryToUser(entry *ldap.Entry) common.User { + return common.User{ + CN: entry.GetAttributeValue("cn"), + SN: entry.GetAttributeValue("sn"), + Mail: entry.GetAttributeValue("mail"), + } +} + +func LDAPEntryToGroup(entry *ldap.Entry) common.Group { + return common.Group{} +} + +func ParseLDAPError(err error) gin.H { + if err != nil { + LDAPerr := err.(*ldap.Error) + return gin.H{ + "ok": false, + "code": LDAPerr.ResultCode, + "result": ldap.LDAPResultCodeMap[LDAPerr.ResultCode], + "message": LDAPerr.Err.Error(), + } + } else { + return gin.H{ + "ok": true, + "code": 200, + "result": "OK", + "message": "", + } + } +} diff --git a/app/main.go b/app/main.go new file mode 100644 index 0000000..85296a0 --- /dev/null +++ b/app/main.go @@ -0,0 +1,166 @@ +package app + +import ( + "crypto/rand" + "log" + "net/http" + "strconv" + + common "user-manager-api/app/common" + ldap "user-manager-api/app/ldap" + "user-manager-api/app/pve" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" + uuid "github.com/nu7hatch/gouuid" +) + +var Version = "0.0.1" +var Config common.Config +var UserSessions map[string]*Backends + +func Run(configPath *string) { + // load config values + Config, err := common.GetConfig(*configPath) + if err != nil { + log.Fatalf("Error when reading config file: %s\n", err) + } + log.Printf("Read in config from %s\n", *configPath) + + // setup router + router := SetupAPI(&Config) + + // make global session map + UserSessions = make(map[string]*Backends) + + router.POST("/ticket", func(c *gin.Context) { + body := common.Login{} + if err := c.ShouldBind(&body); 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 = common.ParseUsername(body.UsernameRaw) + if err != nil { // username format incorrect + c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) + return + } + + // bind proxmox backend + newPVEClient, 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 + } + + // bind ldap backend + newLDAPClient, code, err := ldap.NewClientFromCredentials(Config.LDAP, body.Username, body.Password) + if err != nil { // ldap client failed to bind + c.JSON(code, gin.H{"auth": false, "error": err.Error()}) + return + } + //err = newLDAPClient.BindUser(body.Username, body.Password) + //if err != nil { // failed to authenticate, return error + // c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) + // return + //} + // todo allow ldap backed to fail if user is not using an ldap backend + + // successful binding at this point + // create new session + session := sessions.Default(c) + // create (hopefully) safe uuid to map to ldap session + uuid, _ := uuid.NewV4() + // set uuid mapping in session + session.Set("SessionUUID", uuid.String()) + // set uuid mapping in LDAPSessions + UserSessions[uuid.String()] = &Backends{pve: newPVEClient, ldap: newLDAPClient} + // save the session + session.Save() + // return successful auth + c.JSON(http.StatusOK, gin.H{"auth": true}) + }) + + router.DELETE("/ticket", func(c *gin.Context) { + session := sessions.Default(c) + SessionUUID := session.Get("SessionUUID") + if SessionUUID == nil { + c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) + return + } + uuid := SessionUUID.(string) + delete(UserSessions, uuid) + session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so it is deleted + session.Save() + c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) + }) + + router.GET("/version", func(c *gin.Context) { + c.JSON(200, gin.H{"version": Version}) + }) + + router.POST("/pool/:poolid", func(c *gin.Context) { + poolid, _ := c.Params.Get("poolid") + + backends, code, err := GetBackendsFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } + + code, err = NewPool(backends, poolid) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(200) + } + }) + + router.DELETE("/pool/:poolid", func(c *gin.Context) { + poolid, _ := c.Params.Get("poolid") + + backends, code, err := GetBackendsFromContext(c) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } + + code, err = DelPool(backends, poolid) + if err != nil { + c.JSON(code, gin.H{"error": err.Error()}) + } else { + c.Status(200) + } + }) + + log.Printf("Starting User Manager API on port %s\n", 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 SetupAPI(config *common.Config) *gin.Engine { + secretKey := make([]byte, 256) + n, err := rand.Read(secretKey) + if err != nil { + log.Fatalf("Error when generating session secret key: %s\n", err.Error()) + } + log.Printf("Generated session secret key of length %d\n", n) + + gin.SetMode(gin.ReleaseMode) + router := gin.Default() + store := cookie.NewStore(secretKey) + store.Options(sessions.Options{ + Path: config.SessionCookie.Path, + HttpOnly: config.SessionCookie.HttpOnly, + Secure: config.SessionCookie.Secure, + MaxAge: config.SessionCookie.MaxAge, + }) + router.Use(sessions.Sessions(config.SessionCookieName, store)) + + log.Printf("Started API router and cookie store (Name: %s Params: %+v)\n", config.SessionCookieName, config.SessionCookie) + + return router +} diff --git a/app/operations.go b/app/operations.go new file mode 100644 index 0000000..9d98b61 --- /dev/null +++ b/app/operations.go @@ -0,0 +1,38 @@ +package app + +import ( + common "user-manager-api/app/common" +) + +func NewPool(backends *Backends, poolname string) (int, error) { + return backends.pve.NewPool(poolname) +} +func DelPool(backends *Backends, poolname string) (int, error) { + return backends.pve.DelPool(poolname) +} + +func NewGroup(backends *Backends, groupname common.Groupname) (int, error) { + handler := Config.Realms[groupname.Realm].Handler + switch handler { + case "pve": + return backends.pve.NewGroup(groupname) + case "ldap": + backends.ldap.NewGroup(groupname) + //pve sync + return 200, nil + } + return 200, nil +} + +func DelGroup(backends *Backends, groupname common.Groupname) (int, error) { + handler := Config.Realms[groupname.Realm].Handler + switch handler { + case "pve": + return backends.pve.DelGroup(groupname) + case "ldap": + backends.ldap.DelGroup(groupname) + //pve sync + return 200, nil + } + return 200, nil +} diff --git a/app/pve/pve.go b/app/pve/pve.go new file mode 100644 index 0000000..b10baa0 --- /dev/null +++ b/app/pve/pve.go @@ -0,0 +1,117 @@ +package pve + +import ( + "context" + "crypto/tls" + "net/http" + + common "user-manager-api/app/common" + + "github.com/luthermonson/go-proxmox" +) + +type ProxmoxClient struct { + client *proxmox.Client +} + +func NewClientFromCredentials(config common.PVEConfig, username common.Username, password string) (*ProxmoxClient, int, error) { + HTTPClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + + client := proxmox.NewClient(config.URL, + proxmox.WithHTTPClient(&HTTPClient), + proxmox.WithCredentials(&proxmox.Credentials{Username: username.ToString(), Password: password}), + ) + + // todo this should return an error code if the binding failed (ie fetch version to check if the auth was actually ok) + + return &ProxmoxClient{client: client}, http.StatusOK, nil +} + +func (pve ProxmoxClient) SyncRealms() (int, error) { + domains, err := pve.client.Domains(context.Background()) + if proxmox.IsNotAuthorized(err) { + return 401, err + } else if err != nil { + return 500, err + } + for _, domain := range domains { + if domain.Type != "pam" && domain.Type != "pve" { // pam and pve are not external realm types that require sync + err := domain.Sync(context.Background(), proxmox.DomainSyncOptions{ + DryRun: false, // we want to make modifications + EnableNew: true, // allow new users and groups + Scope: "both", // allow new users and groups + RemoveVanished: "acl;entry;properties", // remove deleted objects from ACL, entry in pve, and remove properties (probably not necessary) + }) + if proxmox.IsNotAuthorized(err) { + return 401, err + } else if err != nil { + return 500, err + } + } + } + return 200, nil +} + +func (pve ProxmoxClient) NewPool(poolname string) (int, error) { + err := pve.client.NewPool(context.Background(), poolname, "") + if proxmox.IsNotAuthorized(err) { + return 401, err + } else if err != nil { + return 500, err + } else { + return 200, nil + } +} + +func (pve ProxmoxClient) DelPool(poolname string) (int, error) { + pvepool, err := pve.client.Pool(context.Background(), poolname) + if proxmox.IsNotFound(err) { // errors if pool does not exist + return 404, err + } else if err != nil { + return 500, err + } + + err = pvepool.Delete(context.Background()) + if proxmox.IsNotAuthorized(err) { // not authorized to delete + return 401, err + } else if err != nil { + return 500, err + } else { + return 200, nil + } +} + +func (pve ProxmoxClient) NewGroup(groupname common.Groupname) (int, error) { + err := pve.client.NewGroup(context.Background(), groupname.ToString(), "") + if proxmox.IsNotAuthorized(err) { + return 401, err + } else if err != nil { + return 500, err + } else { + return 200, nil + } +} + +func (pve ProxmoxClient) DelGroup(groupname common.Groupname) (int, error) { + pvegroup, err := pve.client.Group(context.Background(), groupname.ToString()) + if proxmox.IsNotFound(err) { // errors if group does not exist + return 404, err + } else if err != nil { + return 500, err + } + + err = pvegroup.Delete(context.Background()) + if proxmox.IsNotAuthorized(err) { // not authorized to delete + return 401, err + } else if err != nil { + return 500, err + } else { + return 200, nil + } +} diff --git a/app/utils.go b/app/utils.go new file mode 100644 index 0000000..4579f71 --- /dev/null +++ b/app/utils.go @@ -0,0 +1,55 @@ +package app + +import ( + "encoding/json" + "fmt" + "net/http" + "os" + ldap "user-manager-api/app/ldap" + "user-manager-api/app/pve" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" + + common "user-manager-api/app/common" +) + +type Backends struct { + pve *pve.ProxmoxClient + ldap *ldap.LDAPClient +} + +func GetBackendsFromContext(c *gin.Context) (*Backends, 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 LoadLocaldb(dbPath string) (map[string]common.User, error) { + users := map[string]common.User{} + content, err := os.ReadFile(dbPath) + if err != nil { + //log.Fatal("Error when opening file: ", err) + return users, err + } + err = json.Unmarshal(content, &users) + if err != nil { + //log.Fatal("Error during Unmarshal(): ", err) + return users, err + } + return users, nil +} + +func SaveLocaldb(configPath string, users map[string]common.User) error { + json, err := json.Marshal(users) + if err != nil { + return err + } + err = os.WriteFile(configPath, []byte(json), 0644) + return err +} diff --git a/go.mod b/go.mod index b214a32..2a6a185 100644 --- a/go.mod +++ b/go.mod @@ -1,45 +1,55 @@ module user-manager-api -go 1.25.1 +go 1.26.0 require ( github.com/gin-contrib/sessions v1.0.4 - github.com/gin-gonic/gin v1.10.1 - github.com/go-ldap/ldap/v3 v3.4.11 + github.com/gin-gonic/gin v1.11.0 + github.com/go-ldap/ldap/v3 v3.4.12 + github.com/luthermonson/go-proxmox v0.4.0 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d ) require ( - github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect + github.com/Azure/go-ntlmssp v0.1.0 // indirect + github.com/buger/goterm v1.0.4 // indirect github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.14.1 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/diskfs/go-diskfs v1.7.0 // indirect + github.com/djherbis/times v1.6.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - golang.org/x/arch v0.21.0 // indirect - golang.org/x/crypto v0.42.0 // indirect - golang.org/x/net v0.44.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.24.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/net v0.51.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/main.go b/main.go deleted file mode 100644 index 47e828a..0000000 --- a/main.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -func main() { - gin.SetMode(gin.ReleaseMode) - router := gin.Default() - - router.GET("/version", func(c *gin.Context) { - c.JSON(200, gin.H{ - "version": "0.0.1", - }) - }) - - users := GetLocaldb("localdb.json") - - router.GET("/users", func(c *gin.Context) {}) - router.POST("/users/:userid", func(c *gin.Context) {}) - router.GET("/users/:userid", func(c *gin.Context) { - userid := c.Param("userid") - user, ok := users[userid] - if ok { - c.JSON(http.StatusOK, user) - } else { - c.JSON(http.StatusNotFound, nil) - } - }) - router.DELETE("/users/:userid", func(c *gin.Context) {}) - - router.GET("/groups", func(c *gin.Context) {}) - router.POST("/groups/:groupid", func(c *gin.Context) {}) - router.GET("/groups/:groupid", func(c *gin.Context) {}) - router.DELETE("/groups/:groupid", func(c *gin.Context) {}) - - router.POST("/groups/:groupid/members/:userid", func(c *gin.Context) {}) - router.DELETE("/groups/:groupid/members/:userid", func(c *gin.Context) {}) - - router.Run("0.0.0.0:8083") // listen and serve on 0.0.0.0:8080 -} diff --git a/schema.go b/schema.go deleted file mode 100644 index 2d33024..0000000 --- a/schema.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -/* -type User struct { - Id string - Password string `form:"userpassword" binding:"required"` - CN string `form:"usercn" binding:"required"` - SN string `form:"usersn" binding:"required"` - Resources struct{} - Cluster struct{} - Templates struct{} -} -*/ diff --git a/types.go b/types.go deleted file mode 100644 index 780ac1b..0000000 --- a/types.go +++ /dev/null @@ -1,70 +0,0 @@ -package main - -type User struct { - DN string `json:"dn"` - UID string `json:"uid"` - CN string `json:"cn"` - SN string `json:"sn"` - Mail string `json:"mail"` - MemberOf []string `json:"memberOf"` - Resources map[string]any `json:"resources"` - Cluster Cluster `json:"cluster"` - Templates Templates `json:"templates"` -} - -type Cluster struct { - Admin bool `json:"admin"` - Nodes map[string]bool `json:"nodes"` - VMID VMID `json:"vmid"` - Pools map[string]bool `json:"pools"` - Backups Backups `json:"backups"` -} - -type VMID struct { - Min int `json:"min"` - MAx int `json:"max"` -} - -type Backups struct { - Max int `json:"max"` -} - -type Templates struct { - Instances struct { - LXC map[string]ResourceTemplate `json:"lxc"` - QEMU map[string]ResourceTemplate `json:"qemu"` - } `json:"instances"` -} - -type SimpleResource struct { - Limits struct { - Global SimpleLimit `json:"global"` - Nodes map[string]SimpleLimit `json:"nodes"` - } `json:"limits"` -} - -type SimpleLimit struct { - Max int `json:"max"` -} - -type MatchResource struct { - Limits struct { - Global []MatchLimit `json:"global"` - Nodes map[string][]MatchLimit `json:"nodes"` - } `json:"limits"` -} - -type MatchLimit struct { - Match string `json:"match"` - Name string `json:"name"` - Max int `json:"max"` -} - -type ResourceTemplate struct { - Value string `json:"value"` - Resource struct { - Enabled bool `json:"enabled"` - Name string `json:"name"` - Amount int `json:"amount"` - } `json:"resource"` -} diff --git a/user-manager-api.go b/user-manager-api.go new file mode 100644 index 0000000..eee70f9 --- /dev/null +++ b/user-manager-api.go @@ -0,0 +1,12 @@ +package main + +import ( + "flag" + app "user-manager-api/app" +) + +func main() { + configPath := flag.String("config", "config.json", "path to config.json file") + flag.Parse() + app.Run(configPath) +} diff --git a/utils.go b/utils.go deleted file mode 100644 index aa8c70c..0000000 --- a/utils.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import ( - "encoding/json" - "log" - "os" -) - -func GetLocaldb(dbPath string) map[string]User { - users := map[string]User{} - content, err := os.ReadFile(dbPath) - if err != nil { - log.Fatal("Error when opening file: ", err) - } - err = json.Unmarshal(content, &users) - if err != nil { - log.Fatal("Error during Unmarshal(): ", err) - } - return users -}