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"
)
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) {

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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"),

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {