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
|
||||||
|
}
|
||||||
42
go.mod
42
go.mod
@@ -1,45 +1,55 @@
|
|||||||
module user-manager-api
|
module user-manager-api
|
||||||
|
|
||||||
go 1.25.1
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/sessions v1.0.4
|
github.com/gin-contrib/sessions v1.0.4
|
||||||
github.com/gin-gonic/gin v1.10.1
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/go-ldap/ldap/v3 v3.4.11
|
github.com/go-ldap/ldap/v3 v3.4.12
|
||||||
|
github.com/luthermonson/go-proxmox v0.4.0
|
||||||
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||||
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
github.com/bytedance/sonic v1.14.1 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
github.com/diskfs/go-diskfs v1.7.0 // indirect
|
||||||
|
github.com/djherbis/times v1.6.0 // 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.0 // 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.27.0 // indirect
|
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // 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
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/gorilla/sessions v1.4.0 // indirect
|
github.com/gorilla/sessions v1.4.0 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
|
github.com/jinzhu/copier v0.4.0 // indirect
|
||||||
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.15.0 // 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
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
golang.org/x/arch v0.21.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
golang.org/x/crypto v0.42.0 // indirect
|
golang.org/x/arch v0.24.0 // indirect
|
||||||
golang.org/x/net v0.44.0 // indirect
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
golang.org/x/sys v0.36.0 // indirect
|
golang.org/x/net v0.51.0 // indirect
|
||||||
golang.org/x/text v0.29.0 // indirect
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
golang.org/x/text v0.34.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
43
main.go
43
main.go
@@ -1,43 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
router := gin.Default()
|
|
||||||
|
|
||||||
router.GET("/version", func(c *gin.Context) {
|
|
||||||
c.JSON(200, gin.H{
|
|
||||||
"version": "0.0.1",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
users := GetLocaldb("localdb.json")
|
|
||||||
|
|
||||||
router.GET("/users", func(c *gin.Context) {})
|
|
||||||
router.POST("/users/:userid", func(c *gin.Context) {})
|
|
||||||
router.GET("/users/:userid", func(c *gin.Context) {
|
|
||||||
userid := c.Param("userid")
|
|
||||||
user, ok := users[userid]
|
|
||||||
if ok {
|
|
||||||
c.JSON(http.StatusOK, user)
|
|
||||||
} else {
|
|
||||||
c.JSON(http.StatusNotFound, nil)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
router.DELETE("/users/:userid", func(c *gin.Context) {})
|
|
||||||
|
|
||||||
router.GET("/groups", func(c *gin.Context) {})
|
|
||||||
router.POST("/groups/:groupid", func(c *gin.Context) {})
|
|
||||||
router.GET("/groups/:groupid", func(c *gin.Context) {})
|
|
||||||
router.DELETE("/groups/:groupid", func(c *gin.Context) {})
|
|
||||||
|
|
||||||
router.POST("/groups/:groupid/members/:userid", func(c *gin.Context) {})
|
|
||||||
router.DELETE("/groups/:groupid/members/:userid", func(c *gin.Context) {})
|
|
||||||
|
|
||||||
router.Run("0.0.0.0:8083") // listen and serve on 0.0.0.0:8080
|
|
||||||
}
|
|
||||||
13
schema.go
13
schema.go
@@ -1,13 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
/*
|
|
||||||
type User struct {
|
|
||||||
Id string
|
|
||||||
Password string `form:"userpassword" binding:"required"`
|
|
||||||
CN string `form:"usercn" binding:"required"`
|
|
||||||
SN string `form:"usersn" binding:"required"`
|
|
||||||
Resources struct{}
|
|
||||||
Cluster struct{}
|
|
||||||
Templates struct{}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
70
types.go
70
types.go
@@ -1,70 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
type User struct {
|
|
||||||
DN string `json:"dn"`
|
|
||||||
UID string `json:"uid"`
|
|
||||||
CN string `json:"cn"`
|
|
||||||
SN string `json:"sn"`
|
|
||||||
Mail string `json:"mail"`
|
|
||||||
MemberOf []string `json:"memberOf"`
|
|
||||||
Resources map[string]any `json:"resources"`
|
|
||||||
Cluster Cluster `json:"cluster"`
|
|
||||||
Templates Templates `json:"templates"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cluster struct {
|
|
||||||
Admin bool `json:"admin"`
|
|
||||||
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"`
|
|
||||||
}
|
|
||||||
12
user-manager-api.go
Normal file
12
user-manager-api.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
app "user-manager-api/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configPath := flag.String("config", "config.json", "path to config.json file")
|
||||||
|
flag.Parse()
|
||||||
|
app.Run(configPath)
|
||||||
|
}
|
||||||
20
utils.go
20
utils.go
@@ -1,20 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
func GetLocaldb(dbPath string) map[string]User {
|
|
||||||
users := map[string]User{}
|
|
||||||
content, err := os.ReadFile(dbPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error when opening file: ", err)
|
|
||||||
}
|
|
||||||
err = json.Unmarshal(content, &users)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error during Unmarshal(): ", err)
|
|
||||||
}
|
|
||||||
return users
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user