basic implementation of create/delete pool
This commit is contained in:
47
app/common/config.go
Normal file
47
app/common/config.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
type LDAPConfig struct {
|
||||
LdapURL string `json:"ldapURL"`
|
||||
StartTLS bool `json:"startTLS"`
|
||||
BaseDN string `json:"baseDN"`
|
||||
}
|
||||
|
||||
type PVEConfig struct {
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type RealmConfig struct {
|
||||
Handler string `json:"handler"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
ListenPort int `json:"listenPort"`
|
||||
SessionCookieName string `json:"sessionCookieName"`
|
||||
SessionCookie struct {
|
||||
Path string `json:"path"`
|
||||
HttpOnly bool `json:"httpOnly"`
|
||||
Secure bool `json:"secure"`
|
||||
MaxAge int `json:"maxAge"`
|
||||
}
|
||||
LDAP LDAPConfig `json:"ldap"`
|
||||
PVE PVEConfig `json:"pve"`
|
||||
Realms map[string]RealmConfig `json:"realms"`
|
||||
}
|
||||
|
||||
func GetConfig(configPath string) (Config, error) {
|
||||
content, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
var config Config
|
||||
err = json.Unmarshal(content, &config)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
7
app/common/schema.go
Normal file
7
app/common/schema.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package app
|
||||
|
||||
type Login struct { // login body struct
|
||||
UsernameRaw string `form:"username" binding:"required"`
|
||||
Username Username
|
||||
Password string `form:"password" binding:"required"`
|
||||
}
|
||||
111
app/common/types.go
Normal file
111
app/common/types.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package app
|
||||
|
||||
type BackendClient interface {
|
||||
BindUser(username string, password string) error
|
||||
|
||||
//GetAllUsers() ([]User, int, error)
|
||||
GetUser(username string) (User, int, error)
|
||||
AddUser(username string, user User) (int, error)
|
||||
ModUser(username string, user User) (int, error)
|
||||
DelUser(username string) (int, error)
|
||||
|
||||
//GetAllGroups() ([]Group, int, error)
|
||||
GetGroup(groupname string) (Group, int, error)
|
||||
AddGroup(groupname string, group Group) (int, error)
|
||||
ModGroup(groupname string, group Group) (int, error)
|
||||
DelGroup(groupname string) (int, error)
|
||||
|
||||
AddUserToGroup(username string, groupname string)
|
||||
RemoveUserFromGroup(username string, groupname string)
|
||||
}
|
||||
|
||||
type Pool struct {
|
||||
PoolID string `json:"poolid"`
|
||||
Path string `json:"-"` // typically /pool/poolid from proxmox, only used internally
|
||||
Groups []Group `json:"groups"`
|
||||
Resources map[string]any `json:"resources"`
|
||||
Cluster Cluster `json:"cluster"`
|
||||
Templates Templates `json:"templates"`
|
||||
}
|
||||
|
||||
type Groupname struct { // proxmox typically formats as gid-realm for non pve realms
|
||||
GroupID string `json:"gid"`
|
||||
Realm string `json:"realm"`
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
Groupname Groupname `json:"groupname"`
|
||||
Handler string `json:"-"`
|
||||
Role string `json:"role"`
|
||||
Users []User `json:"users"`
|
||||
}
|
||||
|
||||
type Username struct { // ie userid@realm
|
||||
UserID string `json:"uid"`
|
||||
Realm string `json:"realm"`
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username Username `json:"username"`
|
||||
Handler string `json:"-"`
|
||||
CN string `json:"cn"` // aka first name
|
||||
SN string `json:"sn"` // aka last name
|
||||
Mail string `json:"mail"`
|
||||
Password string `json:"password"` // only used for POST requests
|
||||
}
|
||||
|
||||
type Cluster struct {
|
||||
Nodes map[string]bool `json:"nodes"`
|
||||
VMID VMID `json:"vmid"`
|
||||
//Pools map[string]bool `json:"pools"`
|
||||
Backups Backups `json:"backups"`
|
||||
}
|
||||
|
||||
type VMID struct {
|
||||
Min int `json:"min"`
|
||||
MAx int `json:"max"`
|
||||
}
|
||||
|
||||
type Backups struct {
|
||||
Max int `json:"max"`
|
||||
}
|
||||
|
||||
type Templates struct {
|
||||
Instances struct {
|
||||
LXC map[string]ResourceTemplate `json:"lxc"`
|
||||
QEMU map[string]ResourceTemplate `json:"qemu"`
|
||||
} `json:"instances"`
|
||||
}
|
||||
|
||||
type SimpleResource struct {
|
||||
Limits struct {
|
||||
Global SimpleLimit `json:"global"`
|
||||
Nodes map[string]SimpleLimit `json:"nodes"`
|
||||
} `json:"limits"`
|
||||
}
|
||||
|
||||
type SimpleLimit struct {
|
||||
Max int `json:"max"`
|
||||
}
|
||||
|
||||
type MatchResource struct {
|
||||
Limits struct {
|
||||
Global []MatchLimit `json:"global"`
|
||||
Nodes map[string][]MatchLimit `json:"nodes"`
|
||||
} `json:"limits"`
|
||||
}
|
||||
|
||||
type MatchLimit struct {
|
||||
Match string `json:"match"`
|
||||
Name string `json:"name"`
|
||||
Max int `json:"max"`
|
||||
}
|
||||
|
||||
type ResourceTemplate struct {
|
||||
Value string `json:"value"`
|
||||
Resource struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Name string `json:"name"`
|
||||
Amount int `json:"amount"`
|
||||
} `json:"resource"`
|
||||
}
|
||||
42
app/common/utils.go
Normal file
42
app/common/utils.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseGroupname(groupname string) (Groupname, error) {
|
||||
g := Groupname{}
|
||||
x := strings.Split(groupname, "-")
|
||||
if len(x) == 1 {
|
||||
g.GroupID = groupname
|
||||
g.Realm = "pve"
|
||||
return g, nil
|
||||
} else if len(x) == 2 {
|
||||
g.GroupID = x[0]
|
||||
g.Realm = x[1]
|
||||
return g, nil
|
||||
} else {
|
||||
return g, fmt.Errorf("groupid did not follow the format <groupid> or <groupid>-<realm>")
|
||||
}
|
||||
}
|
||||
|
||||
func ParseUsername(username string) (Username, error) {
|
||||
u := Username{}
|
||||
x := strings.Split(username, "@")
|
||||
if len(x) == 2 {
|
||||
u.UserID = x[0]
|
||||
u.Realm = x[1]
|
||||
return u, nil
|
||||
} else {
|
||||
return u, fmt.Errorf("userid did not follow the format <userid>@<realm>")
|
||||
}
|
||||
}
|
||||
|
||||
func (g Groupname) ToString() string {
|
||||
return fmt.Sprintf("%s-%s", g.GroupID, g.Realm)
|
||||
}
|
||||
|
||||
func (u Username) ToString() string {
|
||||
return fmt.Sprintf("%s-%s", u.UserID, u.Realm)
|
||||
}
|
||||
262
app/ldap/ldap.go
Normal file
262
app/ldap/ldap.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
common "user-manager-api/app/common"
|
||||
)
|
||||
|
||||
// LDAPClient wrapper struct containing the connection, baseDN, peopleDN, and groupsDN
|
||||
type LDAPClient struct {
|
||||
client *ldap.Conn
|
||||
basedn string
|
||||
peopledn string
|
||||
groupsdn string
|
||||
}
|
||||
|
||||
// returns a new LDAPClient from the config
|
||||
func NewClientFromCredentials(config common.LDAPConfig, username common.Username, password string) (*LDAPClient, int, error) {
|
||||
LDAPConn, err := ldap.DialURL(config.LdapURL)
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
|
||||
if config.StartTLS {
|
||||
err = LDAPConn.StartTLS(&tls.Config{InsecureSkipVerify: true})
|
||||
if err != nil {
|
||||
return nil, http.StatusInternalServerError, err
|
||||
}
|
||||
}
|
||||
|
||||
ldap := LDAPClient{
|
||||
client: LDAPConn,
|
||||
basedn: config.BaseDN,
|
||||
peopledn: "ou=people," + config.BaseDN,
|
||||
groupsdn: "ou=groups," + config.BaseDN,
|
||||
}
|
||||
|
||||
userdn := fmt.Sprintf("uid=%s,%s", username.UserID, ldap.peopledn)
|
||||
err = ldap.client.Bind(userdn, password)
|
||||
|
||||
if err != nil {
|
||||
return nil, http.StatusUnauthorized, err
|
||||
} else {
|
||||
return &ldap, http.StatusOK, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (l LDAPClient) GetUser(username common.Username) (common.User, int, error) {
|
||||
user := common.User{}
|
||||
|
||||
searchRequest := ldap.NewSearchRequest( // setup search for user by uid
|
||||
fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn), // The base dn to search
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
"(&(objectClass=inetOrgPerson))", // The filter to apply
|
||||
[]string{"dn", "cn", "sn", "mail", "uid", "memberOf"}, // A list attributes to retrieve
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResponse, err := l.client.Search(searchRequest) // perform search
|
||||
if err != nil {
|
||||
return user, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
entry := searchResponse.Entries[0]
|
||||
|
||||
user = LDAPEntryToUser(entry)
|
||||
|
||||
return user, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) NewUser(username common.Username, user common.User) (int, error) {
|
||||
if user.CN == "" || user.SN == "" || user.Password == "" || user.Mail == "" {
|
||||
return http.StatusBadRequest, ldap.NewError(
|
||||
ldap.LDAPResultUnwillingToPerform,
|
||||
errors.New("missing one of required fields: cn, sn, mail, userpassword"),
|
||||
)
|
||||
}
|
||||
|
||||
addRequest := ldap.NewAddRequest(
|
||||
fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn), // DN
|
||||
nil, // controls
|
||||
)
|
||||
addRequest.Attribute("sn", []string{user.SN})
|
||||
addRequest.Attribute("cn", []string{user.CN})
|
||||
addRequest.Attribute("mail", []string{user.Mail})
|
||||
addRequest.Attribute("userPassword", []string{user.Password})
|
||||
addRequest.Attribute("objectClass", []string{"inetOrgPerson"})
|
||||
|
||||
err := l.client.Add(addRequest)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) ModUser(username common.Username, user common.User) (int, error) {
|
||||
if user.CN == "" && user.SN == "" && user.Password == "" && user.Mail == "" {
|
||||
return http.StatusBadRequest, ldap.NewError(
|
||||
ldap.LDAPResultUnwillingToPerform,
|
||||
errors.New("requires one of fields: cn, sn, mail, userpassword"),
|
||||
)
|
||||
}
|
||||
|
||||
modifyRequest := ldap.NewModifyRequest(
|
||||
fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn),
|
||||
nil,
|
||||
)
|
||||
if user.CN != "" {
|
||||
modifyRequest.Replace("cn", []string{user.CN})
|
||||
}
|
||||
if user.SN != "" {
|
||||
modifyRequest.Replace("sn", []string{user.SN})
|
||||
}
|
||||
if user.Mail != "" {
|
||||
modifyRequest.Replace("mail", []string{user.Mail})
|
||||
}
|
||||
if user.Password != "" {
|
||||
modifyRequest.Replace("userPassword", []string{user.Password})
|
||||
}
|
||||
|
||||
err := l.client.Modify(modifyRequest)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) DelUser(username common.Username) (int, error) {
|
||||
userDN := fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn)
|
||||
|
||||
// assumes that olcMemberOfRefint=true updates member attributes of referenced groups
|
||||
|
||||
deleteUserRequest := ldap.NewDelRequest( // setup delete request
|
||||
userDN,
|
||||
nil,
|
||||
)
|
||||
|
||||
err := l.client.Del(deleteUserRequest) // delete user
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) GetGroup(groupname common.Groupname) (common.Group, int, error) {
|
||||
group := common.Group{}
|
||||
|
||||
searchRequest := ldap.NewSearchRequest( // setup search for user by uid
|
||||
fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn), // The base dn to search
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
"(&(objectClass=groupOfNames))", // The filter to apply
|
||||
[]string{"cn", "member"}, // A list attributes to retrieve
|
||||
nil,
|
||||
)
|
||||
|
||||
searchResponse, err := l.client.Search(searchRequest) // perform search
|
||||
if err != nil {
|
||||
return group, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
entry := searchResponse.Entries[0]
|
||||
group = LDAPEntryToGroup(entry)
|
||||
|
||||
return group, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) NewGroup(groupname common.Groupname) (int, error) {
|
||||
addRequest := ldap.NewAddRequest(
|
||||
fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn), // DN
|
||||
nil, // controls
|
||||
)
|
||||
addRequest.Attribute("cn", []string{groupname.GroupID})
|
||||
addRequest.Attribute("member", []string{""})
|
||||
addRequest.Attribute("objectClass", []string{"groupOfNames"})
|
||||
|
||||
err := l.client.Add(addRequest)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) ModGroup(groupname common.Groupname, group common.Group) (int, error) {
|
||||
modifyRequest := ldap.NewModifyRequest(
|
||||
fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn),
|
||||
nil,
|
||||
)
|
||||
|
||||
modifyRequest.Replace("cn", []string{groupname.GroupID})
|
||||
|
||||
err := l.client.Modify(modifyRequest)
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) DelGroup(groupname common.Groupname) (int, error) {
|
||||
groupDN := fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn)
|
||||
|
||||
// assumes that memberOf overlay will automatically update referenced memberOf attributes
|
||||
|
||||
deleteGroupRequest := ldap.NewDelRequest( // setup delete request
|
||||
groupDN,
|
||||
nil,
|
||||
)
|
||||
|
||||
err := l.client.Del(deleteGroupRequest) // delete group
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) AddUserToGroup(username common.Username, groupname common.Groupname) (int, error) {
|
||||
userDN := fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn)
|
||||
groupDN := fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn)
|
||||
|
||||
modifyRequest := ldap.NewModifyRequest( // modify group member value
|
||||
groupDN,
|
||||
nil,
|
||||
)
|
||||
|
||||
modifyRequest.Add("member", []string{userDN}) // add user to group member attribute
|
||||
|
||||
err := l.client.Modify(modifyRequest) // modify group
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (l LDAPClient) DelUserFromGroup(username common.Username, groupname common.Groupname) (int, error) {
|
||||
userDN := fmt.Sprintf("uid=%s,%s", username.UserID, l.peopledn)
|
||||
groupDN := fmt.Sprintf("cn=%s,%s", groupname.GroupID, l.groupsdn)
|
||||
|
||||
modifyRequest := ldap.NewModifyRequest( // modify group member value
|
||||
groupDN,
|
||||
nil,
|
||||
)
|
||||
|
||||
modifyRequest.Delete("member", []string{userDN}) // remove user from group member attribute
|
||||
|
||||
err := l.client.Modify(modifyRequest) // modify group
|
||||
if err != nil {
|
||||
return http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
39
app/ldap/utils.go
Normal file
39
app/ldap/utils.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
|
||||
common "user-manager-api/app/common"
|
||||
)
|
||||
|
||||
func LDAPEntryToUser(entry *ldap.Entry) common.User {
|
||||
return common.User{
|
||||
CN: entry.GetAttributeValue("cn"),
|
||||
SN: entry.GetAttributeValue("sn"),
|
||||
Mail: entry.GetAttributeValue("mail"),
|
||||
}
|
||||
}
|
||||
|
||||
func LDAPEntryToGroup(entry *ldap.Entry) common.Group {
|
||||
return common.Group{}
|
||||
}
|
||||
|
||||
func ParseLDAPError(err error) gin.H {
|
||||
if err != nil {
|
||||
LDAPerr := err.(*ldap.Error)
|
||||
return gin.H{
|
||||
"ok": false,
|
||||
"code": LDAPerr.ResultCode,
|
||||
"result": ldap.LDAPResultCodeMap[LDAPerr.ResultCode],
|
||||
"message": LDAPerr.Err.Error(),
|
||||
}
|
||||
} else {
|
||||
return gin.H{
|
||||
"ok": true,
|
||||
"code": 200,
|
||||
"result": "OK",
|
||||
"message": "",
|
||||
}
|
||||
}
|
||||
}
|
||||
166
app/main.go
Normal file
166
app/main.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
common "user-manager-api/app/common"
|
||||
ldap "user-manager-api/app/ldap"
|
||||
"user-manager-api/app/pve"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
uuid "github.com/nu7hatch/gouuid"
|
||||
)
|
||||
|
||||
var Version = "0.0.1"
|
||||
var Config common.Config
|
||||
var UserSessions map[string]*Backends
|
||||
|
||||
func Run(configPath *string) {
|
||||
// load config values
|
||||
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
|
||||
router := SetupAPI(&Config)
|
||||
|
||||
// make global session map
|
||||
UserSessions = make(map[string]*Backends)
|
||||
|
||||
router.POST("/ticket", func(c *gin.Context) {
|
||||
body := common.Login{}
|
||||
if err := c.ShouldBind(&body); 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
|
||||
}
|
||||
|
||||
// bind proxmox backend
|
||||
newPVEClient, 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
|
||||
}
|
||||
|
||||
// bind ldap backend
|
||||
newLDAPClient, code, err := ldap.NewClientFromCredentials(Config.LDAP, body.Username, body.Password)
|
||||
if err != nil { // ldap client failed to bind
|
||||
c.JSON(code, gin.H{"auth": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
//err = newLDAPClient.BindUser(body.Username, body.Password)
|
||||
//if err != nil { // failed to authenticate, return error
|
||||
// c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
|
||||
// return
|
||||
//}
|
||||
// todo allow ldap backed to fail if user is not using an ldap backend
|
||||
|
||||
// successful binding at this point
|
||||
// create new session
|
||||
session := sessions.Default(c)
|
||||
// create (hopefully) safe uuid to map to ldap session
|
||||
uuid, _ := uuid.NewV4()
|
||||
// set uuid mapping in session
|
||||
session.Set("SessionUUID", uuid.String())
|
||||
// set uuid mapping in LDAPSessions
|
||||
UserSessions[uuid.String()] = &Backends{pve: newPVEClient, ldap: newLDAPClient}
|
||||
// save the session
|
||||
session.Save()
|
||||
// return successful auth
|
||||
c.JSON(http.StatusOK, gin.H{"auth": true})
|
||||
})
|
||||
|
||||
router.DELETE("/ticket", func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
SessionUUID := session.Get("SessionUUID")
|
||||
if SessionUUID == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||
return
|
||||
}
|
||||
uuid := SessionUUID.(string)
|
||||
delete(UserSessions, uuid)
|
||||
session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so it is deleted
|
||||
session.Save()
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||
})
|
||||
|
||||
router.GET("/version", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"version": Version})
|
||||
})
|
||||
|
||||
router.POST("/pool/:poolid", func(c *gin.Context) {
|
||||
poolid, _ := c.Params.Get("poolid")
|
||||
|
||||
backends, code, err := GetBackendsFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
}
|
||||
|
||||
code, err = NewPool(backends, poolid)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(200)
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/pool/:poolid", func(c *gin.Context) {
|
||||
poolid, _ := c.Params.Get("poolid")
|
||||
|
||||
backends, code, err := GetBackendsFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
}
|
||||
|
||||
code, err = DelPool(backends, 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 SetupAPI(config *common.Config) *gin.Engine {
|
||||
secretKey := make([]byte, 256)
|
||||
n, err := rand.Read(secretKey)
|
||||
if err != nil {
|
||||
log.Fatalf("Error when generating session secret key: %s\n", err.Error())
|
||||
}
|
||||
log.Printf("Generated session secret key of length %d\n", n)
|
||||
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
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 API router and cookie store (Name: %s Params: %+v)\n", config.SessionCookieName, config.SessionCookie)
|
||||
|
||||
return router
|
||||
}
|
||||
38
app/operations.go
Normal file
38
app/operations.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
common "user-manager-api/app/common"
|
||||
)
|
||||
|
||||
func NewPool(backends *Backends, poolname string) (int, error) {
|
||||
return backends.pve.NewPool(poolname)
|
||||
}
|
||||
func DelPool(backends *Backends, poolname string) (int, error) {
|
||||
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":
|
||||
backends.ldap.NewGroup(groupname)
|
||||
//pve sync
|
||||
return 200, nil
|
||||
}
|
||||
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":
|
||||
backends.ldap.DelGroup(groupname)
|
||||
//pve sync
|
||||
return 200, nil
|
||||
}
|
||||
return 200, nil
|
||||
}
|
||||
117
app/pve/pve.go
Normal file
117
app/pve/pve.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package pve
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
|
||||
common "user-manager-api/app/common"
|
||||
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
)
|
||||
|
||||
type ProxmoxClient struct {
|
||||
client *proxmox.Client
|
||||
}
|
||||
|
||||
func NewClientFromCredentials(config common.PVEConfig, username common.Username, password string) (*ProxmoxClient, int, error) {
|
||||
HTTPClient := http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
client := proxmox.NewClient(config.URL,
|
||||
proxmox.WithHTTPClient(&HTTPClient),
|
||||
proxmox.WithCredentials(&proxmox.Credentials{Username: username.ToString(), Password: password}),
|
||||
)
|
||||
|
||||
// todo this should return an error code if the binding failed (ie fetch version to check if the auth was actually ok)
|
||||
|
||||
return &ProxmoxClient{client: client}, http.StatusOK, nil
|
||||
}
|
||||
|
||||
func (pve ProxmoxClient) SyncRealms() (int, error) {
|
||||
domains, err := pve.client.Domains(context.Background())
|
||||
if proxmox.IsNotAuthorized(err) {
|
||||
return 401, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
}
|
||||
for _, domain := range domains {
|
||||
if domain.Type != "pam" && domain.Type != "pve" { // pam and pve are not external realm types that require sync
|
||||
err := domain.Sync(context.Background(), proxmox.DomainSyncOptions{
|
||||
DryRun: false, // we want to make modifications
|
||||
EnableNew: true, // allow new users and groups
|
||||
Scope: "both", // allow new users and groups
|
||||
RemoveVanished: "acl;entry;properties", // remove deleted objects from ACL, entry in pve, and remove properties (probably not necessary)
|
||||
})
|
||||
if proxmox.IsNotAuthorized(err) {
|
||||
return 401, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return 200, nil
|
||||
}
|
||||
|
||||
func (pve ProxmoxClient) NewPool(poolname string) (int, error) {
|
||||
err := pve.client.NewPool(context.Background(), poolname, "")
|
||||
if proxmox.IsNotAuthorized(err) {
|
||||
return 401, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
} else {
|
||||
return 200, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (pve ProxmoxClient) DelPool(poolname string) (int, error) {
|
||||
pvepool, err := pve.client.Pool(context.Background(), poolname)
|
||||
if proxmox.IsNotFound(err) { // errors if pool does not exist
|
||||
return 404, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
}
|
||||
|
||||
err = pvepool.Delete(context.Background())
|
||||
if proxmox.IsNotAuthorized(err) { // not authorized to delete
|
||||
return 401, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
} else {
|
||||
return 200, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (pve ProxmoxClient) NewGroup(groupname common.Groupname) (int, error) {
|
||||
err := pve.client.NewGroup(context.Background(), groupname.ToString(), "")
|
||||
if proxmox.IsNotAuthorized(err) {
|
||||
return 401, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
} else {
|
||||
return 200, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (pve ProxmoxClient) DelGroup(groupname common.Groupname) (int, error) {
|
||||
pvegroup, err := pve.client.Group(context.Background(), groupname.ToString())
|
||||
if proxmox.IsNotFound(err) { // errors if group does not exist
|
||||
return 404, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
}
|
||||
|
||||
err = pvegroup.Delete(context.Background())
|
||||
if proxmox.IsNotAuthorized(err) { // not authorized to delete
|
||||
return 401, err
|
||||
} else if err != nil {
|
||||
return 500, err
|
||||
} else {
|
||||
return 200, nil
|
||||
}
|
||||
}
|
||||
55
app/utils.go
Normal file
55
app/utils.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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"
|
||||
)
|
||||
|
||||
type Backends struct {
|
||||
pve *pve.ProxmoxClient
|
||||
ldap *ldap.LDAPClient
|
||||
}
|
||||
|
||||
func GetBackendsFromContext(c *gin.Context) (*Backends, int, error) {
|
||||
session := sessions.Default(c)
|
||||
SessionUUID := session.Get("SessionUUID")
|
||||
if SessionUUID == nil {
|
||||
return nil, http.StatusUnauthorized, fmt.Errorf("No auth session found")
|
||||
}
|
||||
uuid := SessionUUID.(string)
|
||||
usersession := UserSessions[uuid]
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user