package app import ( "context" "crypto/rand" "crypto/tls" "fmt" "log" "net/http" "strconv" common "user-manager-api/app/common" ldap "user-manager-api/app/ldap" pve "user-manager-api/app/pve" "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]*UserSession var Realms map[string]Realm func Run(configPath *string) { // load config values var err error 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 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]*UserSession) router.GET("/version", func(c *gin.Context) { c.JSON(200, 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 = common.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 ldap backend if backend is ldap if handler == "ldap" { 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 } userbackends.Realm.Name = body.Username.Realm userbackends.Realm.Handler = LDAPClient } // 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 session.Save() // 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) session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so session cookie is deleted session.Save() c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) }) 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(200) } }) 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(200) } }) router.POST("/groups/:groupid", func(c *gin.Context) { groupid, ok := c.Params.Get("groupid") if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("Missing required path parameter groupid")}) return } groupname, err := common.ParseGroupname(groupid) 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(200) } }) router.DELETE("/groups/:groupid", func(c *gin.Context) { groupid, ok := c.Params.Get("groupid") if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("Missing required path parameter groupid")}) return } groupname, err := common.ParseGroupname(groupid) 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(200) } }) router.POST("/pools/:poolid/groups/:groupid", 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 groupid")}) return } groupid, ok := c.Params.Get("groupid") if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("Missing required path parameter groupid")}) return } groupname, err := common.ParseGroupname(groupid) 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(200) } }) router.DELETE("/pools/:poolid/groups/:groupid", 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 groupid")}) return } groupid, ok := c.Params.Get("groupid") if !ok { c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("Missing required path parameter groupid")}) return } groupname, err := common.ParseGroupname(groupid) 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(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 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) 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 cookie store (Name: %s Params: %+v)\n", config.SessionCookieName, config.SessionCookie) 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 }