implement pool group and user get routes,

improvements to http return codes,
add localdb backend handler
This commit is contained in:
2026-04-29 21:15:20 +00:00
parent 136dc90f13
commit de7ac282db
10 changed files with 434 additions and 105 deletions
+6
View File
@@ -16,6 +16,12 @@ type PVEConfig struct {
PAASClientRole string `json:"paas-client-role"` PAASClientRole string `json:"paas-client-role"`
} }
type LDAPConfig struct {
BaseDN string `json:""`
LdapURL string
StartTLS bool
}
type Config struct { type Config struct {
ListenPort int `json:"listenPort"` ListenPort int `json:"listenPort"`
SessionCookieName string `json:"sessionCookieName"` SessionCookieName string `json:"sessionCookieName"`
+11
View File
@@ -4,12 +4,15 @@ import paas "proxmoxaas-common-lib"
type Backend interface { type Backend interface {
NewPool(poolname string) (int, error) NewPool(poolname string) (int, error)
GetPool(poolname string) (Pool, []string, int, error) // []string members
DelPool(poolname string) (int, error) DelPool(poolname string) (int, error)
NewGroup(groupname Groupname) (int, error) NewGroup(groupname Groupname) (int, error)
GetGroup(groupname Groupname) (Group, []string, int, error) // []string members
DelGroup(groupname Groupname) (int, error) DelGroup(groupname Groupname) (int, error)
AddGroupToPool(groupname Groupname, poolname string) (int, error) AddGroupToPool(groupname Groupname, poolname string) (int, error)
DelGroupFromPool(groupname Groupname, poolname string) (int, error) DelGroupFromPool(groupname Groupname, poolname string) (int, error)
NewUser(username Username, user User) (int, error) NewUser(username Username, user User) (int, error)
GetUser(username Username) (User, int, error)
DelUser(username Username) (int, error) DelUser(username Username) (int, error)
AddUserToGroup(username Username, groupname Groupname) (int, error) AddUserToGroup(username Username, groupname Groupname) (int, error)
DelUserFromGroup(username Username, groupname Groupname) (int, error) DelUserFromGroup(username Username, groupname Groupname) (int, error)
@@ -28,3 +31,11 @@ type SimpleLimit = paas.SimpleLimit
type MatchResource = paas.MatchResource type MatchResource = paas.MatchResource
type MatchLimit = paas.MatchLimit type MatchLimit = paas.MatchLimit
type ResourceTemplate = paas.ResourceTemplate type ResourceTemplate = paas.ResourceTemplate
func ParseGroupname(groupname string) (Groupname, error) {
return paas.ParseGroupname(groupname)
}
func ParseUsername(username string) (Username, error) {
return paas.ParseUsername(username)
}
+20 -6
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 *LDAPConfig config *common.LDAPConfig
client *ldap.Conn client *ldap.Conn
} }
// returns a new LDAPClient from the config // returns a new LDAPClient from the config
func NewClientFromCredentials(config LDAPConfig, username common.Username, password string) (*LDAPClient, int, error) { func NewClientFromCredentials(config common.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
@@ -65,6 +65,7 @@ func (l LDAPClient) GetUser(username common.Username) (common.User, int, error)
entry := searchResponse.Entries[0] entry := searchResponse.Entries[0]
user = LDAPEntryToUser(entry) user = LDAPEntryToUser(entry)
user.Username = username
return user, http.StatusOK, nil return user, http.StatusOK, nil
} }
@@ -146,8 +147,9 @@ func (l LDAPClient) DelUser(username common.Username) (int, error) {
return http.StatusOK, nil return http.StatusOK, nil
} }
func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, int, error) { func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, []string, int, error) {
group := common.Group{} group := common.Group{}
members := []string{}
searchRequest := ldap.NewSearchRequest( // setup search for user by uid searchRequest := ldap.NewSearchRequest( // setup search for user by uid
fmt.Sprintf("cn=%s,ou=groups,%s", groupname.GroupID, l.config.BaseDN), // The base dn to search fmt.Sprintf("cn=%s,ou=groups,%s", groupname.GroupID, l.config.BaseDN), // The base dn to search
@@ -159,13 +161,18 @@ func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, int, err
searchResponse, err := l.client.Search(searchRequest) // perform search searchResponse, err := l.client.Search(searchRequest) // perform search
if err != nil { if err != nil {
return group, http.StatusBadRequest, err return group, members, http.StatusBadRequest, err
} }
entry := searchResponse.Entries[0] entry := searchResponse.Entries[0]
group = LDAPEntryToGroup(entry) group = LDAPEntryToGroup(entry)
group.Groupname = groupname
return group, http.StatusOK, nil for _, member := range entry.GetAttributeValues("member") {
if member != "" {
members = append(members, member)
}
}
return group, members, http.StatusOK, nil
} }
func (l LDAPClient) NewGroup(groupname common.Groupname) (int, error) { func (l LDAPClient) NewGroup(groupname common.Groupname) (int, error) {
@@ -261,12 +268,19 @@ func (l LDAPClient) DelUserFromGroup(username common.Username, groupname common.
func (l LDAPClient) NewPool(poolname string) (int, error) { func (l LDAPClient) NewPool(poolname string) (int, error) {
return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools")
} }
func (l LDAPClient) GetPool(poolname string) (common.Pool, []string, int, error) {
return common.Pool{}, []string{}, http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools")
}
func (l LDAPClient) DelPool(poolname string) (int, error) { func (l LDAPClient) DelPool(poolname string) (int, error) {
return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools")
} }
func (l LDAPClient) AddGroupToPool(groupname common.Groupname, poolname string) (int, error) { func (l LDAPClient) AddGroupToPool(groupname common.Groupname, poolname string) (int, error) {
return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools")
} }
func (l LDAPClient) DelGroupFromPool(groupname common.Groupname, poolname string) (int, error) { func (l LDAPClient) DelGroupFromPool(groupname common.Groupname, poolname string) (int, error) {
return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools") return http.StatusNotImplemented, fmt.Errorf("ldap does not implement pools")
} }
+14 -7
View File
@@ -1,18 +1,16 @@
package ldap package ldap
import ( import (
"fmt"
"net/http"
"regexp"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
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"),
@@ -37,9 +35,18 @@ func ParseLDAPError(err error) gin.H {
} else { } else {
return gin.H{ return gin.H{
"ok": true, "ok": true,
"code": 200, "code": http.StatusOK,
"result": "OK", "result": "OK",
"message": "", "message": "",
} }
} }
} }
func ExtractUIDFromUserDN(dn string) (string, error) {
m := regexp.MustCompilePOSIX("uid=([[:alnum:]]+)")
x := m.FindStringSubmatch(dn)
if len(x) != 2 {
return "", fmt.Errorf("could not find uid in dn %s", dn)
}
return x[1], nil
}
+46
View File
@@ -0,0 +1,46 @@
package localdb
import (
"encoding/json"
"fmt"
"net/http"
"os"
common "user-manager-api/app/common"
)
type DB struct {
data map[string]common.Pool
}
func LoadDB(localDBPath string) (DB, error) {
db := DB{}
content, err := os.ReadFile(localDBPath)
if err != nil {
//log.Fatal("Error when opening file: ", err)
return db, err
}
err = json.Unmarshal(content, &db.data)
if err != nil {
//log.Fatal("Error during Unmarshal(): ", err)
return db, err
}
return db, nil
}
func SaveDB(localDBPath string, db DB) error {
json, err := json.Marshal(db.data)
if err != nil {
return err
}
err = os.WriteFile(localDBPath, []byte(json), 0644)
return err
}
func (localdb DB) GetPool(poolname string) (common.Pool, []string, int, error) {
pool := common.Pool{}
pool, ok := localdb.data[poolname]
if !ok {
return pool, []string{}, http.StatusNotFound, fmt.Errorf("Pool %s not in localdb", poolname)
}
return pool, []string{}, http.StatusOK, nil
}
+97 -14
View File
@@ -12,6 +12,7 @@ import (
paas "proxmoxaas-common-lib" paas "proxmoxaas-common-lib"
common "user-manager-api/app/common" common "user-manager-api/app/common"
ldap "user-manager-api/app/ldap" ldap "user-manager-api/app/ldap"
localdb "user-manager-api/app/localdb"
pve "user-manager-api/app/pve" pve "user-manager-api/app/pve"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
@@ -26,7 +27,7 @@ var Config common.Config
var UserSessions map[string]*UserSession var UserSessions map[string]*UserSession
var Realms map[string]Realm var Realms map[string]Realm
func Run(configPath *string) { func Run(configPath *string, localDBPath *string) {
// load config values // load config values
var err error var err error
Config, err = common.GetConfig(*configPath) Config, err = common.GetConfig(*configPath)
@@ -35,6 +36,13 @@ func Run(configPath *string) {
} }
log.Printf("Read in config from %s\n", *configPath) log.Printf("Read in config from %s\n", *configPath)
// load localdb
db, err := localdb.LoadDB(*localDBPath)
if err != nil {
log.Fatalf("Error when reading localdb file: %s\n", err)
}
log.Printf("Read in localdb from %s\n", *localDBPath)
// setup router // setup router
gin.SetMode(gin.ReleaseMode) gin.SetMode(gin.ReleaseMode)
router := SetupAPISessionStore(&Config) router := SetupAPISessionStore(&Config)
@@ -47,7 +55,7 @@ func Run(configPath *string) {
UserSessions = make(map[string]*UserSession) 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(http.StatusOK, gin.H{"version": Version})
}) })
router.POST("/ticket", func(c *gin.Context) { router.POST("/ticket", func(c *gin.Context) {
@@ -78,7 +86,7 @@ func Run(configPath *string) {
// bind ldap backend if backend is ldap // bind ldap backend if backend is ldap
if handler == "ldap" { if handler == "ldap" {
config := Realms[body.Username.Realm].Config.(ldap.LDAPConfig) config := Realms[body.Username.Realm].Config.(common.LDAPConfig)
LDAPClient, code, err := ldap.NewClientFromCredentials(config, body.Username, body.Password) 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()})
@@ -88,6 +96,8 @@ func Run(configPath *string) {
userbackends.Realm.Handler = LDAPClient userbackends.Realm.Handler = LDAPClient
} }
userbackends.DB = &db
// successful binding at this point // successful binding at this point
// create new session // create new session
session := sessions.Default(c) session := sessions.Default(c)
@@ -120,6 +130,27 @@ func Run(configPath *string) {
c.JSON(http.StatusUnauthorized, gin.H{"auth": false}) c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
}) })
router.GET("/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
}
pool, code, err := GetPool(backends, poolid)
if err != nil {
c.JSON(code, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusOK, gin.H{"pool": pool})
}
})
router.POST("/pools/:poolid", func(c *gin.Context) { router.POST("/pools/:poolid", func(c *gin.Context) {
poolid, ok := c.Params.Get("poolid") poolid, ok := c.Params.Get("poolid")
if !ok { if !ok {
@@ -137,7 +168,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -158,7 +189,33 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
}
})
router.GET("/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 poolid")})
return
}
groupname, err := common.ParseGroupname(groupid)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
backends, code, err := GetUserSessionFromContext(c)
if err != nil {
c.JSON(code, gin.H{"error": err.Error()})
return
}
group, code, err := GetGroup(backends, groupname)
if err != nil {
c.JSON(code, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusOK, gin.H{"group": group})
} }
}) })
@@ -185,7 +242,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -212,7 +269,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -245,7 +302,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -278,7 +335,33 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
}
})
router.GET("/users/:userid", func(c *gin.Context) {
userid, ok := c.Params.Get("userid")
if !ok {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("Missing required path parameter poolid")})
return
}
username, err := common.ParseUsername(userid)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
backends, code, err := GetUserSessionFromContext(c)
if err != nil {
c.JSON(code, gin.H{"error": err.Error()})
return
}
user, code, err := GetUser(backends, username)
if err != nil {
c.JSON(code, gin.H{"error": err.Error()})
} else {
c.JSON(http.StatusOK, gin.H{"user": user})
} }
}) })
@@ -318,7 +401,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -345,7 +428,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -384,7 +467,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -423,7 +506,7 @@ func Run(configPath *string) {
if err != nil { if err != nil {
c.JSON(code, gin.H{"error": err.Error()}) c.JSON(code, gin.H{"error": err.Error()})
} else { } else {
c.Status(200) c.Status(http.StatusOK)
} }
}) })
@@ -496,7 +579,7 @@ func GetRealmsFromPVE(config *common.Config) map[string]Realm {
} }
if realm.Type == "ldap" { if realm.Type == "ldap" {
ldapconfig := ldap.LDAPConfig{ ldapconfig := common.LDAPConfig{
BaseDN: realm.BaseDN, BaseDN: realm.BaseDN,
LdapURL: fmt.Sprintf("ldap://%s", realm.Server1), LdapURL: fmt.Sprintf("ldap://%s", realm.Server1),
StartTLS: realm.Mode == "ldap+starttls", StartTLS: realm.Mode == "ldap+starttls",
+112
View File
@@ -4,12 +4,51 @@ import (
"fmt" "fmt"
"net/http" "net/http"
common "user-manager-api/app/common" common "user-manager-api/app/common"
"user-manager-api/app/ldap"
) )
func NewPool(backends *UserSession, 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)
} }
// get pool recursive resolving groups
func GetPool(backends *UserSession, poolname string) (common.Pool, int, error) {
pool := common.Pool{}
// get pool from PVE
pvepool, members, code, err := backends.PVE.GetPool(poolname)
if err != nil {
return pool, code, err
}
// get pool from DB
dbpool, _, code, err := backends.DB.GetPool(poolname)
if err != nil {
return pool, code, err
}
// assign pool id from PVE, assign everything else from DB
pool.PoolID = pvepool.PoolID
pool.Resources = dbpool.Resources
pool.AllowedNodes = dbpool.AllowedNodes
pool.AllowedVMIDRange = dbpool.AllowedVMIDRange
pool.AllowedBackups = dbpool.AllowedBackups
pool.Templates = dbpool.Templates
// pool members are groups
for _, groupid := range members {
groupname, err := common.ParseGroupname(groupid)
if err != nil {
return pool, code, fmt.Errorf("pool %s had member %s which is not a valid groupname", poolname, groupid)
}
group, code, err := GetGroup(backends, groupname)
if err != nil {
return pool, code, err
}
group.Role = Config.PVE.PAASClientRole
pool.Groups = append(pool.Groups, group)
}
return pool, http.StatusOK, nil
}
func DelPool(backends *UserSession, 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)
@@ -30,6 +69,60 @@ func NewGroup(backends *UserSession, groupname common.Groupname) (int, error) {
} }
} }
func GetGroup(backends *UserSession, groupname common.Groupname) (common.Group, int, error) {
// resolve group from relevant backend
if groupname.Realm == "pve" {
group, members, code, err := backends.PVE.GetGroup(groupname)
if err != nil {
return group, code, err
}
// group members are users
for _, userid := range members {
username, err := common.ParseUsername(userid)
if err != nil {
return group, http.StatusInternalServerError, fmt.Errorf("group %s had member %s which is not a valid username", groupname.ToString(), userid)
}
// fetch and append user
user, code, err := GetUser(backends, username)
if err != nil {
return group, code, err
}
group.Users = append(group.Users, user)
}
return group, http.StatusOK, nil
} else if groupname.Realm == backends.Realm.Name {
group, members, code, err := backends.Realm.Handler.(common.Backend).GetGroup(groupname)
if err != nil {
return common.Group{}, code, err
}
// group mambers are users
for _, userdn := range members {
// member list is of ldap user DN instead of pve userid
ldapuid, err := ldap.ExtractUIDFromUserDN(userdn)
if err != nil {
return group, http.StatusInternalServerError, fmt.Errorf("group %s had member %s which is not a valid user DN", groupname.ToString(), userdn)
}
// generate username from user DN (slightly inefficient)
userid := fmt.Sprintf("%s@%s", ldapuid, backends.Realm.Name)
username, err := common.ParseUsername(userid)
if err != nil {
return group, http.StatusInternalServerError, fmt.Errorf("group %s had member %s which is not a valid username", groupname.ToString(), userid)
}
// fetch and append user
user, code, err := GetUser(backends, username)
if err != nil {
return group, code, err
}
group.Users = append(group.Users, user)
}
return group, http.StatusOK, nil
} else {
return common.Group{}, http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested group")
}
}
func DelGroup(backends *UserSession, groupname common.Groupname) (int, error) { func DelGroup(backends *UserSession, groupname common.Groupname) (int, error) {
if groupname.Realm == "pve" { if groupname.Realm == "pve" {
return backends.PVE.DelGroup(groupname) return backends.PVE.DelGroup(groupname)
@@ -70,6 +163,25 @@ func NewUser(backends *UserSession, username common.Username, user common.User)
} }
} }
func GetUser(backends *UserSession, username common.Username) (common.User, int, error) {
// fetch user from relevant realm
if username.Realm == "pve" {
pveuser, code, err := backends.PVE.GetUser(username)
if err != nil {
return common.User{}, code, err
}
return pveuser, http.StatusOK, nil
} else if username.Realm == backends.Realm.Name {
realmuser, code, err := backends.Realm.Handler.(common.Backend).GetUser(username)
if err != nil {
return common.User{}, code, err
}
return realmuser, http.StatusOK, nil
} else {
return common.User{}, http.StatusUnauthorized, fmt.Errorf("user is not in the same realm as requested user")
}
}
func DelUser(backends *UserSession, username common.Username) (int, error) { func DelUser(backends *UserSession, username common.Username) (int, error) {
if username.Realm == "pve" { if username.Realm == "pve" {
return backends.PVE.DelUser(username) return backends.PVE.DelUser(username)
+124 -50
View File
@@ -39,16 +39,15 @@ func NewClientFromCredentials(config common.PVEConfig, username common.Username,
return nil, http.StatusUnauthorized, err return nil, http.StatusUnauthorized, err
} }
// todo this should return an error code if the binding failed (ie fetch version to check if the auth was actually ok)
return &ProxmoxClient{config: &config, client: client}, http.StatusOK, nil return &ProxmoxClient{config: &config, client: client}, http.StatusOK, nil
} }
func (pve ProxmoxClient) SyncRealms() (int, error) { func (pve ProxmoxClient) SyncRealms() (int, error) {
domains, err := pve.client.Domains(context.Background()) domains, err := pve.client.Domains(context.Background())
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
for _, domain := range domains { for _, domain := range domains {
if domain.Type != "pam" && domain.Type != "pve" { // pam and pve are not external realm types that require sync if domain.Type != "pam" && domain.Type != "pve" { // pam and pve are not external realm types that require sync
@@ -59,41 +58,73 @@ func (pve ProxmoxClient) SyncRealms() (int, error) {
RemoveVanished: "acl;entry;properties", // remove deleted objects from ACL, entry in pve, and remove properties (probably not necessary) RemoveVanished: "acl;entry;properties", // remove deleted objects from ACL, entry in pve, and remove properties (probably not necessary)
}) })
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
} }
} }
return 200, nil return http.StatusOK, nil
} }
func (pve ProxmoxClient) NewPool(poolname string) (int, error) { func (pve ProxmoxClient) NewPool(poolname string) (int, error) {
err := pve.client.NewPool(context.Background(), poolname, "") err := pve.client.NewPool(context.Background(), poolname, "")
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
func (pve ProxmoxClient) GetPool(poolname string) (common.Pool, []string, int, error) {
pool := common.Pool{}
members := []string{}
pvepool, err := pve.client.Pool(context.Background(), poolname)
if proxmox.IsNotFound(err) { // errors if pool does not exist
return pool, members, http.StatusNotFound, err
} else if err != nil {
return pool, members, http.StatusInternalServerError, err
}
pool.PoolID = pvepool.PoolID
acls, err := pve.client.ACL(context.Background())
if proxmox.IsNotAuthorized(err) { // errors if pool does not exist
return pool, members, http.StatusUnauthorized, err
} else if err != nil {
return pool, members, http.StatusInternalServerError, err
}
// iterate through ACLs and get superficial pool - group membershipm (just group names)
for _, acl := range acls {
if acl.Type == "group" && acl.RoleID == pve.config.PAASClientRole && acl.Path == fmt.Sprintf("/pool/%s", poolname) {
members = append(members, acl.UGID)
}
}
return pool, members, http.StatusOK, nil
}
func (pve ProxmoxClient) DelPool(poolname string) (int, error) { func (pve ProxmoxClient) DelPool(poolname string) (int, error) {
pvepool, err := pve.client.Pool(context.Background(), poolname) pvepool, err := pve.client.Pool(context.Background(), poolname)
if proxmox.IsNotFound(err) { // errors if pool does not exist if proxmox.IsNotAuthorized(err) { // not authorized to delete
return 404, err return http.StatusUnauthorized, err
} else if proxmox.IsNotFound(err) { // errors if pool does not exist
return http.StatusNotFound, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
err = pvepool.Delete(context.Background()) err = pvepool.Delete(context.Background())
if proxmox.IsNotAuthorized(err) { // not authorized to delete if proxmox.IsNotAuthorized(err) { // not authorized to delete
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
@@ -101,29 +132,50 @@ func (pve ProxmoxClient) NewGroup(groupname common.Groupname) (int, error) {
// add new group ny ID only // add new group ny ID only
err := pve.client.NewGroup(context.Background(), groupname.GroupID, "") err := pve.client.NewGroup(context.Background(), groupname.GroupID, "")
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
func (pve ProxmoxClient) GetGroup(groupname common.Groupname) (common.Group, []string, int, error) {
group := common.Group{}
members := []string{}
pvegroup, err := pve.client.Group(context.Background(), groupname.ToString())
if proxmox.IsNotFound(err) { // errors if pool does not exist
return group, members, http.StatusNotFound, err
} else if err != nil {
return group, members, http.StatusInternalServerError, err
}
group.Groupname, _ = common.ParseGroupname(pvegroup.GroupID)
for _, userid := range pvegroup.Members {
members = append(members, userid)
}
return group, members, http.StatusOK, nil
}
func (pve ProxmoxClient) DelGroup(groupname common.Groupname) (int, error) { func (pve ProxmoxClient) DelGroup(groupname common.Groupname) (int, error) {
pvegroup, err := pve.client.Group(context.Background(), groupname.GroupID) pvegroup, err := pve.client.Group(context.Background(), groupname.GroupID)
if proxmox.IsNotFound(err) { // errors if group does not exist if proxmox.IsNotAuthorized(err) {
return 404, err return http.StatusUnauthorized, err
} else if proxmox.IsNotFound(err) {
return http.StatusNotFound, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
err = pvegroup.Delete(context.Background()) err = pvegroup.Delete(context.Background())
if proxmox.IsNotAuthorized(err) { // not authorized to delete if proxmox.IsNotAuthorized(err) { // not authorized to delete
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
@@ -137,11 +189,11 @@ func (pve ProxmoxClient) AddGroupToPool(groupname common.Groupname, poolname str
}) })
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
@@ -155,11 +207,11 @@ func (pve ProxmoxClient) DelGroupFromPool(groupname common.Groupname, poolname s
}) })
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
@@ -172,39 +224,59 @@ func (pve ProxmoxClient) NewUser(username common.Username, user common.User) (in
Password: user.Password, Password: user.Password,
}) })
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err return http.StatusUnauthorized, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
}
}
func (pve ProxmoxClient) GetUser(username common.Username) (common.User, int, error) {
user := common.User{}
pveuser, err := pve.client.User(context.Background(), username.ToString())
if proxmox.IsNotFound(err) { // errors if pool does not exist
return user, http.StatusNotFound, err
} else if err != nil {
return user, http.StatusInternalServerError, err
} else {
user.Username, _ = common.ParseUsername(pveuser.UserID)
user.CN = pveuser.Firstname
user.SN = pveuser.Lastname
user.Mail = pveuser.Email
return user, http.StatusOK, nil
} }
} }
func (pve ProxmoxClient) DelUser(username common.Username) (int, error) { func (pve ProxmoxClient) DelUser(username common.Username) (int, error) {
user, err := pve.client.User(context.Background(), username.ToString()) user, err := pve.client.User(context.Background(), username.ToString())
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err // not authorized to read the user = not authorized to delete the user, this may be slightly different from ldap return http.StatusUnauthorized, err
} else if proxmox.IsNotFound(err) {
return http.StatusNotFound, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
// assume that user cannot be nil if no error was returned // assume that user cannot be nil if no error was returned
err = user.Delete(context.Background()) err = user.Delete(context.Background())
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err // not authorized to delete the user return http.StatusUnauthorized, err // not authorized to delete the user
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
func (pve ProxmoxClient) AddUserToGroup(username common.Username, groupname common.Groupname) (int, error) { func (pve ProxmoxClient) AddUserToGroup(username common.Username, groupname common.Groupname) (int, error) {
user, err := pve.client.User(context.Background(), groupname.ToString()) user, err := pve.client.User(context.Background(), username.ToString())
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err // not authorized to read the user = not authorized to delete the user, this may be slightly different from ldap return http.StatusUnauthorized, err
} else if proxmox.IsNotFound(err) {
return http.StatusNotFound, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
newGroups := append(user.Groups, groupname.ToString()) newGroups := append(user.Groups, groupname.ToString())
@@ -213,20 +285,22 @@ func (pve ProxmoxClient) AddUserToGroup(username common.Username, groupname comm
Groups: newGroups, Groups: newGroups,
}) })
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err // not authorized to delete the user return http.StatusUnauthorized, err // not authorized to delete the user
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
func (pve ProxmoxClient) DelUserFromGroup(username common.Username, groupname common.Groupname) (int, error) { func (pve ProxmoxClient) DelUserFromGroup(username common.Username, groupname common.Groupname) (int, error) {
user, err := pve.client.User(context.Background(), groupname.ToString()) user, err := pve.client.User(context.Background(), username.ToString())
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err // not authorized to read the user = not authorized to delete the user, this may be slightly different from ldap return http.StatusUnauthorized, err
} else if proxmox.IsNotFound(err) {
return http.StatusNotFound, err
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} }
idx := slices.Index(user.Groups, groupname.ToString()) idx := slices.Index(user.Groups, groupname.ToString())
@@ -239,10 +313,10 @@ func (pve ProxmoxClient) DelUserFromGroup(username common.Username, groupname co
Groups: newGroups, Groups: newGroups,
}) })
if proxmox.IsNotAuthorized(err) { if proxmox.IsNotAuthorized(err) {
return 401, err // not authorized to delete the user return http.StatusUnauthorized, err // not authorized to delete the user
} else if err != nil { } else if err != nil {
return 500, err return http.StatusInternalServerError, err
} else { } else {
return 200, nil return http.StatusOK, nil
} }
} }
+2 -27
View File
@@ -1,15 +1,13 @@
package app package app
import ( import (
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"os"
"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" localdb "user-manager-api/app/localdb"
pve "user-manager-api/app/pve" pve "user-manager-api/app/pve"
) )
@@ -24,6 +22,7 @@ type UserSession struct {
Name string Name string
Handler any Handler any
} }
DB *localdb.DB
} }
func GetUserSessionFromContext(c *gin.Context) (*UserSession, int, error) { func GetUserSessionFromContext(c *gin.Context) (*UserSession, int, error) {
@@ -36,27 +35,3 @@ func GetUserSessionFromContext(c *gin.Context) (*UserSession, int, error) {
usersession := UserSessions[uuid] usersession := UserSessions[uuid]
return usersession, http.StatusOK, nil 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
}
+2 -1
View File
@@ -7,6 +7,7 @@ import (
func main() { func main() {
configPath := flag.String("config", "config.json", "path to config.json file") configPath := flag.String("config", "config.json", "path to config.json file")
localDBPath := flag.String("localdb", "localdb.json", "path to localdb.json file")
flag.Parse() flag.Parse()
app.Run(configPath) app.Run(configPath, localDBPath)
} }