diff --git a/app/common/config.go b/app/common/config.go index 3b572cb..9aeb358 100644 --- a/app/common/config.go +++ b/app/common/config.go @@ -5,21 +5,17 @@ import ( "os" ) -type LDAPConfig struct { - LdapURL string `json:"ldapURL"` - StartTLS bool `json:"startTLS"` - BaseDN string `json:"baseDN"` -} - type PVEConfig struct { - URL string `json:"url"` + URL string `json:"url"` + Token struct { + User string `json:"user"` + Realm string `json:"realm"` + ID string `json:"id"` + UUID string `json:"uuid"` + } `json:"token"` PAASClientRole string `json:"paas-client-role"` } -type RealmConfig struct { - Handler string `json:"handler"` -} - type Config struct { ListenPort int `json:"listenPort"` SessionCookieName string `json:"sessionCookieName"` @@ -29,9 +25,7 @@ type Config struct { Secure bool `json:"secure"` MaxAge int `json:"maxAge"` } - LDAP LDAPConfig `json:"ldap"` - PVE PVEConfig `json:"pve"` - Realms map[string]RealmConfig `json:"realms"` + PVE PVEConfig `json:"pve"` } func GetConfig(configPath string) (Config, error) { diff --git a/app/common/types.go b/app/common/types.go index 00bc59e..0b4986b 100644 --- a/app/common/types.go +++ b/app/common/types.go @@ -1,5 +1,18 @@ package app +type Backend interface { + NewPool(poolname string) (int, error) + DelPool(poolname string) (int, error) + NewGroup(groupname Groupname) (int, error) + 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) + DelUser(username Username) (int, error) + AddUserToGroup(username Username, groupname Groupname) (int, error) + DelUserFromGroup(username Username, groupname Groupname) (int, error) +} + type Pool struct { PoolID string `json:"poolid"` Path string `json:"-"` // typically /pool/poolid from proxmox, only used internally diff --git a/app/ldap/ldap.go b/app/ldap/ldap.go index 345ba4f..c8de7c4 100644 --- a/app/ldap/ldap.go +++ b/app/ldap/ldap.go @@ -13,12 +13,12 @@ import ( // LDAPClient wrapper struct containing the connection, baseDN, peopleDN, and groupsDN type LDAPClient struct { - config *common.LDAPConfig + config *LDAPConfig client *ldap.Conn } // returns a new LDAPClient from the config -func NewClientFromCredentials(config common.LDAPConfig, username common.Username, password string) (*LDAPClient, int, error) { +func NewClientFromCredentials(config LDAPConfig, username common.Username, password string) (*LDAPClient, int, error) { LDAPConn, err := ldap.DialURL(config.LdapURL) if err != nil { return nil, http.StatusInternalServerError, err @@ -257,3 +257,16 @@ func (l LDAPClient) DelUserFromGroup(username common.Username, groupname common. return http.StatusOK, nil } + +func (l LDAPClient) NewPool(poolname string) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") +} +func (l LDAPClient) DelPool(poolname string) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") +} +func (l LDAPClient) AddGroupToPool(groupname common.Groupname, poolname string) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") +} +func (l LDAPClient) DelGroupFromPool(groupname common.Groupname, poolname string) (int, error) { + return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") +} diff --git a/app/ldap/utils.go b/app/ldap/utils.go index f92370c..9ca7f14 100644 --- a/app/ldap/utils.go +++ b/app/ldap/utils.go @@ -7,6 +7,12 @@ import ( common "user-manager-api/app/common" ) +type LDAPConfig struct { + BaseDN string + LdapURL string + StartTLS bool +} + func LDAPEntryToUser(entry *ldap.Entry) common.User { return common.User{ CN: entry.GetAttributeValue("cn"), diff --git a/app/main.go b/app/main.go index 066dfdb..fd9be37 100644 --- a/app/main.go +++ b/app/main.go @@ -1,7 +1,9 @@ package app import ( + "context" "crypto/rand" + "crypto/tls" "fmt" "log" "net/http" @@ -14,12 +16,14 @@ import ( "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" ) var Version = "0.0.1" var Config common.Config -var UserSessions map[string]*Backends +var UserSessions map[string]*UserSession +var Realms map[string]Realm func Run(configPath *string) { // load config values @@ -34,8 +38,12 @@ func Run(configPath *string) { gin.SetMode(gin.ReleaseMode) router := SetupAPISessionStore(&Config) + // get realms from proxmox + Realms = make(map[string]Realm) + Realms = GetRealmsFromPVE(&Config) + // make global session map - UserSessions = make(map[string]*Backends) + UserSessions = make(map[string]*UserSession) router.GET("/version", func(c *gin.Context) { c.JSON(200, gin.H{"version": Version}) @@ -55,7 +63,9 @@ func Run(configPath *string) { c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()}) return } - handler := Config.Realms[body.Username.Realm].Handler + handler := Realms[body.Username.Realm].Type + + userbackends := UserSession{} // always bind proxmox backend PVEClient, code, err := pve.NewClientFromCredentials(Config.PVE, body.Username, body.Password) @@ -63,16 +73,19 @@ func Run(configPath *string) { c.JSON(code, gin.H{"auth": false, "error": err.Error()}) return } + userbackends.PVE = PVEClient // bind ldap backend if backend is ldap - var LDAPClient *ldap.LDAPClient if handler == "ldap" { - LDAPClient, code, err = ldap.NewClientFromCredentials(Config.LDAP, body.Username, body.Password) + config := Realms[body.Username.Realm].Config.(ldap.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 } - } //ldap client will be nil if it is unused!! + userbackends.Realm.Name = body.Username.Realm + userbackends.Realm.Handler = LDAPClient + } // successful binding at this point // create new session @@ -82,7 +95,7 @@ func Run(configPath *string) { // set uuid mapping in session session.Set("SessionUUID", uuid.String()) // set uuid mapping in LDAPSessions - UserSessions[uuid.String()] = &Backends{handler: handler, pve: PVEClient, ldap: LDAPClient} + UserSessions[uuid.String()] = &userbackends // save the session session.Save() // return successful auth @@ -113,7 +126,7 @@ func Run(configPath *string) { return } - backends, code, err := GetBackendsFromContext(c) + backends, code, err := GetUserSessionFromContext(c) if err != nil { c.JSON(code, gin.H{"error": err.Error()}) return @@ -134,7 +147,7 @@ func Run(configPath *string) { return } - backends, code, err := GetBackendsFromContext(c) + backends, code, err := GetUserSessionFromContext(c) if err != nil { c.JSON(code, gin.H{"error": err.Error()}) return @@ -160,7 +173,7 @@ func Run(configPath *string) { return } - backends, code, err := GetBackendsFromContext(c) + backends, code, err := GetUserSessionFromContext(c) if err != nil { c.JSON(code, gin.H{"error": err.Error()}) return @@ -186,7 +199,7 @@ func Run(configPath *string) { return } - backends, code, err := GetBackendsFromContext(c) + backends, code, err := GetUserSessionFromContext(c) if err != nil { c.JSON(code, gin.H{"error": err.Error()}) return @@ -218,7 +231,7 @@ func Run(configPath *string) { return } - backends, code, err := GetBackendsFromContext(c) + backends, code, err := GetUserSessionFromContext(c) if err != nil { c.JSON(code, gin.H{"error": err.Error()}) return @@ -250,7 +263,7 @@ func Run(configPath *string) { return } - backends, code, err := GetBackendsFromContext(c) + backends, code, err := GetUserSessionFromContext(c) if err != nil { c.JSON(code, gin.H{"error": err.Error()}) return @@ -291,3 +304,62 @@ func SetupAPISessionStore(config *common.Config) *gin.Engine { return router } + +func GetRealmsFromPVE(config *common.Config) map[string]Realm { + realms := map[string]Realm{} + + HTTPClient := http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: true, + }, + }, + } + token := fmt.Sprintf(`%s@%s!%s`, config.PVE.Token.User, config.PVE.Token.Realm, config.PVE.Token.ID) + client := proxmox.NewClient(config.PVE.URL, + proxmox.WithHTTPClient(&HTTPClient), + proxmox.WithAPIToken(token, config.PVE.Token.UUID), + ) + + pverealms, err := client.Domains(context.Background()) + if err != nil { + // failure to get realms is a fatal error + log.Fatalf("Error getting authentication realms: %s", err.Error()) + } + + // add required pve realm handler, removing the pve api token + pveconfig := common.PVEConfig{ + URL: config.PVE.URL, + PAASClientRole: config.PVE.PAASClientRole, + } + realms["pve"] = Realm{ + Type: "pve", + Config: pveconfig, + } + log.Printf("Configured default authentication realm pve") + + // iterate through handlers and + for _, r := range pverealms { + realm, err := client.Domain(context.Background(), r.Realm) + if err != nil { + log.Printf("Error getting authentication realm %s: %s", r.Realm, err.Error()) + } + + if realm.Type == "ldap" { + ldapconfig := ldap.LDAPConfig{ + BaseDN: realm.BaseDN, + LdapURL: fmt.Sprintf("ldap://%s", realm.Server1), + StartTLS: realm.Mode == "ldap+starttls", + } + realms[realm.Realm] = Realm{ + Type: realm.Type, + Config: ldapconfig, + } + log.Printf("Configured external authentication realm %s", realm.Realm) + } else { + continue + } + } + + return realms +} diff --git a/app/operations.go b/app/operations.go index 3baa5c0..ea16249 100644 --- a/app/operations.go +++ b/app/operations.go @@ -1,126 +1,120 @@ package app import ( + "fmt" + "net/http" common "user-manager-api/app/common" ) -func NewPool(backends *Backends, poolname string) (int, error) { +func NewPool(backends *UserSession, poolname string) (int, error) { // only pve backend handles pools - return backends.pve.NewPool(poolname) + return backends.PVE.NewPool(poolname) } -func DelPool(backends *Backends, poolname string) (int, error) { +func DelPool(backends *UserSession, poolname string) (int, error) { // only pve backend handles pools - return backends.pve.DelPool(poolname) + 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": - code, err := backends.ldap.NewGroup(groupname) +func NewGroup(backends *UserSession, groupname common.Groupname) (int, error) { + if groupname.Realm == "pve" { + return backends.PVE.NewGroup(groupname) + } else if groupname.Realm == backends.Realm.Name { + realm_handler := backends.Realm.Handler.(common.Backend) + code, err := realm_handler.NewGroup(groupname) if err != nil { return code, err } - - //pve sync - return backends.pve.SyncRealms() + return backends.PVE.SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested group") } - 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": - code, err := backends.ldap.DelGroup(groupname) +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) + code, err := realm_handler.DelGroup(groupname) if err != nil { return code, err } - - //pve sync - return backends.pve.SyncRealms() + return backends.PVE.SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested group") } - return 200, nil } -func AddGroupToPool(backends *Backends, groupname common.Groupname, poolname string) (int, error) { +func AddGroupToPool(backends *UserSession, groupname common.Groupname, poolname string) (int, error) { // only pve backend handles pool-group membership - return backends.pve.AddGroupToPool(groupname, poolname) + return backends.PVE.AddGroupToPool(groupname, poolname) } -func DelGroupFromPool(backends *Backends, groupname common.Groupname, poolname string) (int, error) { +func DelGroupFromPool(backends *UserSession, groupname common.Groupname, poolname string) (int, error) { // only pve backend handles pool-group membership - return backends.pve.DelGroupFromPool(groupname, poolname) + return backends.PVE.DelGroupFromPool(groupname, poolname) } -func NewUser(backends *Backends, username common.Username, user common.User) (int, error) { - handler := Config.Realms[username.Realm].Handler - switch handler { - case "pve": - return backends.pve.NewUser(username, user) - case "ldap": - code, err := backends.ldap.NewUser(username, user) +func NewUser(backends *UserSession, username common.Username, user common.User) (int, error) { + if username.Realm == "pve" { + return backends.PVE.NewUser(username, user) + } else if username.Realm == backends.Realm.Name { + realm_handler := backends.Realm.Handler.(common.Backend) + code, err := realm_handler.NewUser(username, user) if err != nil { return code, err } - - //pve sync - return backends.pve.SyncRealms() + return backends.PVE.SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested user") } - return 200, nil } -func DelUser(backends *Backends, username common.Username) (int, error) { - handler := Config.Realms[username.Realm].Handler - switch handler { - case "pve": - return backends.pve.DelUser(username) - case "ldap": - code, err := backends.ldap.DelUser(username) +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) + code, err := realm_handler.DelUser(username) if err != nil { return code, err } - - //pve sync - return backends.pve.SyncRealms() + return backends.PVE.SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested user") } - return 200, nil } -func AddUserToGroup(backends *Backends, username common.Username, groupname common.Groupname) (int, error) { - handler := Config.Realms[username.Realm].Handler - switch handler { - case "pve": - return backends.pve.AddUserToGroup(username, groupname) - case "ldap": - code, err := backends.ldap.AddUserToGroup(username, groupname) +func AddUserToGroup(backends *UserSession, username common.Username, groupname common.Groupname) (int, error) { + if username.Realm == "pve" && groupname.Realm == "pve" { // both requested user and requested group are in proxmox + return backends.PVE.AddUserToGroup(username, groupname) + } else if username.Realm == backends.Realm.Name && groupname.Realm == "pve" { // requested user is in user's realm but group is in proxmox + return backends.PVE.AddUserToGroup(username, groupname) + } else if username.Realm == backends.Realm.Name && groupname.Realm == backends.Realm.Name { // both requested user and requested group are in user's realm + realm_handler := backends.Realm.Handler.(common.Backend) + code, err := realm_handler.AddUserToGroup(username, groupname) if err != nil { return code, err } - - //pve sync - return backends.pve.SyncRealms() + return backends.PVE.SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("cannot add a pve user to a group in %s", groupname.Realm) } - return 200, nil } -func DelUserFromGroup(backends *Backends, username common.Username, groupname common.Groupname) (int, error) { - handler := Config.Realms[username.Realm].Handler - switch handler { - case "pve": - return backends.pve.DelUserFromGroup(username, groupname) - case "ldap": - code, err := backends.ldap.DelUserFromGroup(username, groupname) +func DelUserFromGroup(backends *UserSession, username common.Username, groupname common.Groupname) (int, error) { + if username.Realm == "pve" && groupname.Realm == "pve" { // both requested user and requested group are in proxmox + return backends.PVE.DelUserFromGroup(username, groupname) + } else if username.Realm == backends.Realm.Name && groupname.Realm == "pve" { // requested user is in user's realm but group is in proxmox + return backends.PVE.DelUserFromGroup(username, groupname) + } else if username.Realm == backends.Realm.Name && groupname.Realm == backends.Realm.Name { // both requested user and requested group are in user's realm + realm_handler := backends.Realm.Handler.(common.Backend) + code, err := realm_handler.DelUserFromGroup(username, groupname) if err != nil { return code, err } - - //pve sync - return backends.pve.SyncRealms() + return backends.PVE.SyncRealms() + } else { + return http.StatusUnauthorized, fmt.Errorf("cannot remove a pve user from a group in %s", groupname.Realm) } - return 200, nil } diff --git a/app/utils.go b/app/utils.go index a1fcd6d..a840c4f 100644 --- a/app/utils.go +++ b/app/utils.go @@ -5,22 +5,28 @@ import ( "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" + pve "user-manager-api/app/pve" ) -type Backends struct { - handler string - pve *pve.ProxmoxClient - ldap *ldap.LDAPClient +type Realm struct { + Type string + Config any } -func GetBackendsFromContext(c *gin.Context) (*Backends, int, error) { +type UserSession struct { + PVE *pve.ProxmoxClient + Realm struct { + Name string + Handler any + } +} + +func GetUserSessionFromContext(c *gin.Context) (*UserSession, int, error) { session := sessions.Default(c) SessionUUID := session.Get("SessionUUID") if SessionUUID == nil { diff --git a/go.mod b/go.mod index 299a57e..720dcc9 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module user-manager-api go 1.26.0 require ( - github.com/gin-contrib/sessions v1.0.4 + github.com/gin-contrib/sessions v1.1.0 github.com/gin-gonic/gin v1.12.0 github.com/go-ldap/ldap/v3 v3.4.13 - github.com/luthermonson/go-proxmox v0.4.0 + github.com/luthermonson/go-proxmox v0.4.1 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d ) @@ -15,16 +15,16 @@ require ( github.com/buger/goterm v1.0.4 // indirect github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/bytedance/sonic/loader v0.5.1 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/diskfs/go-diskfs v1.8.0 // indirect + github.com/diskfs/go-diskfs v1.9.1 // 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/gin-contrib/sse v1.1.1 // 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.30.1 // indirect + github.com/go-playground/validator/v10 v10.30.2 // indirect github.com/goccy/go-json v0.10.6 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/uuid v1.6.0 // indirect @@ -36,7 +36,7 @@ require ( 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.16.1 // indirect + github.com/magefile/mage v1.17.1 // 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