create realm configurations from proxmox domain configuration using api token,

update go mod with domain sync fixes from go-proxmox
This commit is contained in:
2026-04-04 22:24:15 +00:00
parent 97bd582505
commit 3e3723a208
8 changed files with 215 additions and 117 deletions

View File

@@ -5,21 +5,17 @@ import (
"os" "os"
) )
type LDAPConfig struct {
LdapURL string `json:"ldapURL"`
StartTLS bool `json:"startTLS"`
BaseDN string `json:"baseDN"`
}
type PVEConfig struct { 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"` PAASClientRole string `json:"paas-client-role"`
} }
type RealmConfig struct {
Handler string `json:"handler"`
}
type Config struct { type Config struct {
ListenPort int `json:"listenPort"` ListenPort int `json:"listenPort"`
SessionCookieName string `json:"sessionCookieName"` SessionCookieName string `json:"sessionCookieName"`
@@ -29,9 +25,7 @@ type Config struct {
Secure bool `json:"secure"` Secure bool `json:"secure"`
MaxAge int `json:"maxAge"` MaxAge int `json:"maxAge"`
} }
LDAP LDAPConfig `json:"ldap"`
PVE PVEConfig `json:"pve"` PVE PVEConfig `json:"pve"`
Realms map[string]RealmConfig `json:"realms"`
} }
func GetConfig(configPath string) (Config, error) { func GetConfig(configPath string) (Config, error) {

View File

@@ -1,5 +1,18 @@
package app 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 { type Pool struct {
PoolID string `json:"poolid"` PoolID string `json:"poolid"`
Path string `json:"-"` // typically /pool/poolid from proxmox, only used internally Path string `json:"-"` // typically /pool/poolid from proxmox, only used internally

View File

@@ -13,12 +13,12 @@ import (
// LDAPClient wrapper struct containing the connection, baseDN, peopleDN, and groupsDN // LDAPClient wrapper struct containing the connection, baseDN, peopleDN, and groupsDN
type LDAPClient struct { type LDAPClient struct {
config *common.LDAPConfig config *LDAPConfig
client *ldap.Conn client *ldap.Conn
} }
// returns a new LDAPClient from the config // 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) LDAPConn, err := ldap.DialURL(config.LdapURL)
if err != nil { if err != nil {
return nil, http.StatusInternalServerError, err return nil, http.StatusInternalServerError, err
@@ -257,3 +257,16 @@ func (l LDAPClient) DelUserFromGroup(username common.Username, groupname common.
return http.StatusOK, nil 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")
}

View File

@@ -7,6 +7,12 @@ import (
common "user-manager-api/app/common" common "user-manager-api/app/common"
) )
type LDAPConfig struct {
BaseDN string
LdapURL string
StartTLS bool
}
func LDAPEntryToUser(entry *ldap.Entry) common.User { func LDAPEntryToUser(entry *ldap.Entry) common.User {
return common.User{ return common.User{
CN: entry.GetAttributeValue("cn"), CN: entry.GetAttributeValue("cn"),

View File

@@ -1,7 +1,9 @@
package app package app
import ( import (
"context"
"crypto/rand" "crypto/rand"
"crypto/tls"
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
@@ -14,12 +16,14 @@ import (
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/luthermonson/go-proxmox"
uuid "github.com/nu7hatch/gouuid" uuid "github.com/nu7hatch/gouuid"
) )
var Version = "0.0.1" var Version = "0.0.1"
var Config common.Config var Config common.Config
var UserSessions map[string]*Backends var UserSessions map[string]*UserSession
var Realms map[string]Realm
func Run(configPath *string) { func Run(configPath *string) {
// load config values // load config values
@@ -34,8 +38,12 @@ func Run(configPath *string) {
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
router := SetupAPISessionStore(&Config) router := SetupAPISessionStore(&Config)
// get realms from proxmox
Realms = make(map[string]Realm)
Realms = GetRealmsFromPVE(&Config)
// make global session map // make global session map
UserSessions = make(map[string]*Backends) UserSessions = make(map[string]*UserSession)
router.GET("/version", func(c *gin.Context) { router.GET("/version", func(c *gin.Context) {
c.JSON(200, gin.H{"version": Version}) 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()}) c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
return return
} }
handler := Config.Realms[body.Username.Realm].Handler handler := Realms[body.Username.Realm].Type
userbackends := UserSession{}
// always bind proxmox backend // always bind proxmox backend
PVEClient, code, err := pve.NewClientFromCredentials(Config.PVE, body.Username, body.Password) 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()}) c.JSON(code, gin.H{"auth": false, "error": err.Error()})
return return
} }
userbackends.PVE = PVEClient
// bind ldap backend if backend is ldap // bind ldap backend if backend is ldap
var LDAPClient *ldap.LDAPClient
if handler == "ldap" { 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 if err != nil { // ldap client failed to bind
c.JSON(code, gin.H{"auth": false, "error": err.Error()}) c.JSON(code, gin.H{"auth": false, "error": err.Error()})
return 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 // successful binding at this point
// create new session // create new session
@@ -82,7 +95,7 @@ func Run(configPath *string) {
// set uuid mapping in session // set uuid mapping in session
session.Set("SessionUUID", uuid.String()) session.Set("SessionUUID", uuid.String())
// set uuid mapping in LDAPSessions // set uuid mapping in LDAPSessions
UserSessions[uuid.String()] = &Backends{handler: handler, pve: PVEClient, ldap: LDAPClient} UserSessions[uuid.String()] = &userbackends
// save the session // save the session
session.Save() session.Save()
// return successful auth // return successful auth
@@ -113,7 +126,7 @@ func Run(configPath *string) {
return return
} }
backends, code, err := GetBackendsFromContext(c) backends, code, err := GetUserSessionFromContext(c)
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
return return
@@ -134,7 +147,7 @@ func Run(configPath *string) {
return return
} }
backends, code, err := GetBackendsFromContext(c) backends, code, err := GetUserSessionFromContext(c)
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
return return
@@ -160,7 +173,7 @@ func Run(configPath *string) {
return return
} }
backends, code, err := GetBackendsFromContext(c) backends, code, err := GetUserSessionFromContext(c)
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
return return
@@ -186,7 +199,7 @@ func Run(configPath *string) {
return return
} }
backends, code, err := GetBackendsFromContext(c) backends, code, err := GetUserSessionFromContext(c)
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
return return
@@ -218,7 +231,7 @@ func Run(configPath *string) {
return return
} }
backends, code, err := GetBackendsFromContext(c) backends, code, err := GetUserSessionFromContext(c)
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
return return
@@ -250,7 +263,7 @@ func Run(configPath *string) {
return return
} }
backends, code, err := GetBackendsFromContext(c) backends, code, err := GetUserSessionFromContext(c)
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
return return
@@ -291,3 +304,62 @@ func SetupAPISessionStore(config *common.Config) *gin.Engine {
return router 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
}

View File

@@ -1,126 +1,120 @@
package app package app
import ( import (
"fmt"
"net/http"
common "user-manager-api/app/common" 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 // 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 // only pve backend handles pools
return backends.pve.DelPool(poolname) return backends.PVE.DelPool(poolname)
} }
func NewGroup(backends *Backends, groupname common.Groupname) (int, error) { func NewGroup(backends *UserSession, groupname common.Groupname) (int, error) {
handler := Config.Realms[groupname.Realm].Handler if groupname.Realm == "pve" {
switch handler { return backends.PVE.NewGroup(groupname)
case "pve": } else if groupname.Realm == backends.Realm.Name {
return backends.pve.NewGroup(groupname) realm_handler := backends.Realm.Handler.(common.Backend)
case "ldap": code, err := realm_handler.NewGroup(groupname)
code, err := backends.ldap.NewGroup(groupname)
if err != nil { if err != nil {
return code, err return code, err
} }
return backends.PVE.SyncRealms()
//pve sync } else {
return backends.pve.SyncRealms() 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) { func DelGroup(backends *UserSession, groupname common.Groupname) (int, error) {
handler := Config.Realms[groupname.Realm].Handler if groupname.Realm == "pve" {
switch handler { return backends.PVE.DelGroup(groupname)
case "pve": } else if groupname.Realm == backends.Realm.Name {
return backends.pve.DelGroup(groupname) realm_handler := backends.Realm.Handler.(common.Backend)
case "ldap": code, err := realm_handler.DelGroup(groupname)
code, err := backends.ldap.DelGroup(groupname)
if err != nil { if err != nil {
return code, err return code, err
} }
return backends.PVE.SyncRealms()
//pve sync } else {
return backends.pve.SyncRealms() 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 // 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 // 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) { func NewUser(backends *UserSession, username common.Username, user common.User) (int, error) {
handler := Config.Realms[username.Realm].Handler if username.Realm == "pve" {
switch handler { return backends.PVE.NewUser(username, user)
case "pve": } else if username.Realm == backends.Realm.Name {
return backends.pve.NewUser(username, user) realm_handler := backends.Realm.Handler.(common.Backend)
case "ldap": code, err := realm_handler.NewUser(username, user)
code, err := backends.ldap.NewUser(username, user)
if err != nil { if err != nil {
return code, err return code, err
} }
return backends.PVE.SyncRealms()
//pve sync } else {
return backends.pve.SyncRealms() 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) { func DelUser(backends *UserSession, username common.Username) (int, error) {
handler := Config.Realms[username.Realm].Handler if username.Realm == "pve" {
switch handler { return backends.PVE.DelUser(username)
case "pve": } else if username.Realm == backends.Realm.Name {
return backends.pve.DelUser(username) realm_handler := backends.Realm.Handler.(common.Backend)
case "ldap": code, err := realm_handler.DelUser(username)
code, err := backends.ldap.DelUser(username)
if err != nil { if err != nil {
return code, err return code, err
} }
return backends.PVE.SyncRealms()
//pve sync } else {
return backends.pve.SyncRealms() 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) { func AddUserToGroup(backends *UserSession, username common.Username, groupname common.Groupname) (int, error) {
handler := Config.Realms[username.Realm].Handler if username.Realm == "pve" && groupname.Realm == "pve" { // both requested user and requested group are in proxmox
switch handler { return backends.PVE.AddUserToGroup(username, groupname)
case "pve": } 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) return backends.PVE.AddUserToGroup(username, groupname)
case "ldap": } else if username.Realm == backends.Realm.Name && groupname.Realm == backends.Realm.Name { // both requested user and requested group are in user's realm
code, err := backends.ldap.AddUserToGroup(username, groupname) realm_handler := backends.Realm.Handler.(common.Backend)
code, err := realm_handler.AddUserToGroup(username, groupname)
if err != nil { if err != nil {
return code, err return code, err
} }
return backends.PVE.SyncRealms()
//pve sync } else {
return backends.pve.SyncRealms() 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) { func DelUserFromGroup(backends *UserSession, username common.Username, groupname common.Groupname) (int, error) {
handler := Config.Realms[username.Realm].Handler if username.Realm == "pve" && groupname.Realm == "pve" { // both requested user and requested group are in proxmox
switch handler { return backends.PVE.DelUserFromGroup(username, groupname)
case "pve": } 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) return backends.PVE.DelUserFromGroup(username, groupname)
case "ldap": } else if username.Realm == backends.Realm.Name && groupname.Realm == backends.Realm.Name { // both requested user and requested group are in user's realm
code, err := backends.ldap.DelUserFromGroup(username, groupname) realm_handler := backends.Realm.Handler.(common.Backend)
code, err := realm_handler.DelUserFromGroup(username, groupname)
if err != nil { if err != nil {
return code, err return code, err
} }
return backends.PVE.SyncRealms()
//pve sync } else {
return backends.pve.SyncRealms() return http.StatusUnauthorized, fmt.Errorf("cannot remove a pve user from a group in %s", groupname.Realm)
} }
return 200, nil
} }

View File

@@ -5,22 +5,28 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
ldap "user-manager-api/app/ldap"
"user-manager-api/app/pve"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
common "user-manager-api/app/common" common "user-manager-api/app/common"
pve "user-manager-api/app/pve"
) )
type Backends struct { type Realm struct {
handler string Type string
pve *pve.ProxmoxClient Config any
ldap *ldap.LDAPClient
} }
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) session := sessions.Default(c)
SessionUUID := session.Get("SessionUUID") SessionUUID := session.Get("SessionUUID")
if SessionUUID == nil { if SessionUUID == nil {

14
go.mod
View File

@@ -3,10 +3,10 @@ module user-manager-api
go 1.26.0 go 1.26.0
require ( 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/gin-gonic/gin v1.12.0
github.com/go-ldap/ldap/v3 v3.4.13 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 github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
) )
@@ -15,16 +15,16 @@ require (
github.com/buger/goterm v1.0.4 // indirect github.com/buger/goterm v1.0.4 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // 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/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/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // 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-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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-json v0.10.6 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/uuid v1.6.0 // 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/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.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/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect