use json encoding for route body parameters, implement modify user/group/pool operations, simplify schema by using comon lib struct tags and RequireAll and AtLeastOne helper functions, properly implement locadb as Backend interface
This commit is contained in:
+53
-488
@@ -11,18 +11,27 @@ import (
|
||||
"strconv"
|
||||
|
||||
common "access-manager-api/app/common"
|
||||
ldap "access-manager-api/app/ldap"
|
||||
localdb "access-manager-api/app/localdb"
|
||||
pve "access-manager-api/app/pve"
|
||||
paas "proxmoxaas-common-lib"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luthermonson/go-proxmox"
|
||||
uuid "github.com/nu7hatch/gouuid"
|
||||
)
|
||||
|
||||
type Realm struct {
|
||||
Type string
|
||||
Config any
|
||||
}
|
||||
|
||||
type UserSession struct {
|
||||
PVE common.Backend
|
||||
Realm struct {
|
||||
Name string
|
||||
Handler common.Backend
|
||||
}
|
||||
DB common.Backend
|
||||
}
|
||||
|
||||
var Version = "1.0.0"
|
||||
var Config common.Config
|
||||
var UserSessions map[string]*UserSession
|
||||
@@ -30,25 +39,19 @@ var Realms map[string]Realm
|
||||
|
||||
func Run() {
|
||||
configPath := flag.String("config", "config.json", "path to config.json file")
|
||||
localDBPath := flag.String("localdb", "localdb.json", "path to localdb.json file")
|
||||
flag.Parse()
|
||||
|
||||
// load config values
|
||||
var err error
|
||||
Config = common.GetConfig(*configPath)
|
||||
// already exits if failed
|
||||
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 gin
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
router := SetupAPISessionStore(&Config)
|
||||
router := gin.Default()
|
||||
|
||||
// setup api auth cookies
|
||||
SetupAPISessionStore(router, &Config)
|
||||
|
||||
// get realms from proxmox
|
||||
Realms = make(map[string]Realm)
|
||||
@@ -61,486 +64,39 @@ func Run() {
|
||||
c.JSON(http.StatusOK, gin.H{"version": Version})
|
||||
})
|
||||
|
||||
router.POST("/ticket", func(c *gin.Context) {
|
||||
body := common.Login{}
|
||||
err := c.ShouldBind(&body)
|
||||
if 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 = paas.ParseUsername(body.UsernameRaw)
|
||||
if err != nil { // username format incorrect
|
||||
c.JSON(http.StatusBadRequest, gin.H{"auth": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
handler := Realms[body.Username.Realm].Type
|
||||
|
||||
userbackends := UserSession{}
|
||||
|
||||
// always bind proxmox backend
|
||||
PVEClient, 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
|
||||
}
|
||||
userbackends.PVE = PVEClient
|
||||
|
||||
// bind backend by type
|
||||
switch handler {
|
||||
case "pve":
|
||||
case "ldap":
|
||||
config := Realms[body.Username.Realm].Config.(common.LDAPConfig)
|
||||
LDAPClient, code, err := ldap.NewClientFromCredentials(config, body.Username, body.Password)
|
||||
if err != nil { // ldap client failed to bind
|
||||
c.JSON(code, gin.H{"auth": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
userbackends.Realm.Name = body.Username.Realm
|
||||
userbackends.Realm.Handler = LDAPClient
|
||||
default:
|
||||
c.JSON(code, gin.H{"auth": false, "error": fmt.Errorf("user realm %s is not supported", body.Username.Realm)})
|
||||
return
|
||||
}
|
||||
|
||||
userbackends.DB = &db
|
||||
|
||||
// successful binding at this point
|
||||
// create new session
|
||||
session := sessions.Default(c)
|
||||
// create random uuid to map user to backends
|
||||
uuid, _ := uuid.NewV4()
|
||||
// set uuid mapping in session
|
||||
session.Set("SessionUUID", uuid.String())
|
||||
// set uuid mapping in LDAPSessions
|
||||
UserSessions[uuid.String()] = &userbackends
|
||||
// save the session
|
||||
err = session.Save()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"auth": false})
|
||||
} else {
|
||||
// return successful auth
|
||||
c.JSON(http.StatusOK, gin.H{"auth": true})
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/ticket", func(c *gin.Context) {
|
||||
// get session uuid from session cookie
|
||||
session := sessions.Default(c)
|
||||
SessionUUID := session.Get("SessionUUID")
|
||||
if SessionUUID == nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"auth": false})
|
||||
return
|
||||
}
|
||||
uuid := SessionUUID.(string)
|
||||
|
||||
// delete uuid entry from user sessions
|
||||
delete(UserSessions, uuid) // deletes uuid mapping
|
||||
session.Options(sessions.Options{MaxAge: -1}) // set max age to -1 so session cookie is deleted
|
||||
err := session.Save() // if save somehow fails, it should be ok since the uuid mapping is already deleted
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"auth": false})
|
||||
} else {
|
||||
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) {
|
||||
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
|
||||
}
|
||||
|
||||
code, err = NewPool(backends, poolid)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/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
|
||||
}
|
||||
|
||||
code, err = DelPool(backends, poolid)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.GET("/groups/:groupname", func(c *gin.Context) {
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := common.ParseGroupname(groupname_str)
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
router.POST("/groups/:groupname", func(c *gin.Context) {
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := paas.ParseGroupname(groupname_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = NewGroup(backends, groupname)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/groups/:groupname", func(c *gin.Context) {
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := paas.ParseGroupname(groupname_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = DelGroup(backends, groupname)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.POST("/pools/:poolid/groups/:groupname", 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
|
||||
}
|
||||
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := paas.ParseGroupname(groupname_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = AddGroupToPool(backends, groupname, poolid)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/pools/:poolid/groups/:groupname", 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
|
||||
}
|
||||
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := paas.ParseGroupname(groupname_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = DelGroupFromPool(backends, groupname, poolid)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.GET("/users/:username", func(c *gin.Context) {
|
||||
username_str, ok := c.Params.Get("username")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter poolid")})
|
||||
return
|
||||
}
|
||||
|
||||
username, err := common.ParseUsername(username_str)
|
||||
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})
|
||||
}
|
||||
})
|
||||
|
||||
router.POST("/users/:username", func(c *gin.Context) {
|
||||
username_str, ok := c.Params.Get("username")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
username, err := paas.ParseUsername(username_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
form := common.UserFormRequired{}
|
||||
err = c.ShouldBind(&form)
|
||||
if err != nil { // failed binding, usually missing form field
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user := common.User{}
|
||||
user.CN = form.CN
|
||||
user.SN = form.SN
|
||||
user.Mail = form.Mail
|
||||
user.Password = form.Password
|
||||
|
||||
code, err = NewUser(backends, username, user)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/users/:username", func(c *gin.Context) {
|
||||
username_str, ok := c.Params.Get("username")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
username, err := paas.ParseUsername(username_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = DelUser(backends, username)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.POST("/groups/:groupname/users/:username", func(c *gin.Context) {
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
username_str, ok := c.Params.Get("username")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter username")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := paas.ParseGroupname(groupname_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
username, err := paas.ParseUsername(username_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = AddUserToGroup(backends, username, groupname)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
|
||||
router.DELETE("/groups/:groupname/users/:username", func(c *gin.Context) {
|
||||
groupname_str, ok := c.Params.Get("groupname")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter groupname")})
|
||||
return
|
||||
}
|
||||
|
||||
username_str, ok := c.Params.Get("username")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("missing required path parameter username")})
|
||||
return
|
||||
}
|
||||
|
||||
groupname, err := paas.ParseGroupname(groupname_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
username, err := paas.ParseUsername(username_str)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err})
|
||||
return
|
||||
}
|
||||
|
||||
backends, code, err := GetUserSessionFromContext(c)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
code, err = DelUserFromGroup(backends, username, groupname)
|
||||
if err != nil {
|
||||
c.JSON(code, gin.H{"error": err.Error()})
|
||||
} else {
|
||||
c.Status(http.StatusOK)
|
||||
}
|
||||
})
|
||||
router.POST("/ticket", POST_Ticket)
|
||||
router.DELETE("/ticket", DELETE_Ticket)
|
||||
router.GET("/pools/:poolid", GET_Pool)
|
||||
router.POST("/pools/:poolid", POST_Pool)
|
||||
router.DELETE("/pools/:poolid", DELETE_Pool)
|
||||
router.GET("/groups/:groupname", GET_Group)
|
||||
router.POST("/groups/:groupname", POST_Group)
|
||||
router.DELETE("/groups/:groupname", DELETE_Group)
|
||||
router.POST("/pools/:poolid/groups/:groupname", POST_Pool_Group)
|
||||
router.DELETE("/pools/:poolid/groups/:groupname")
|
||||
router.GET("/users/:username", GET_User)
|
||||
router.POST("/users/:username", POST_User)
|
||||
router.DELETE("/users/:username", DELETE_User)
|
||||
router.POST("/groups/:groupname/users/:username", POST_Group_User)
|
||||
router.DELETE("/groups/:groupname/users/:username", DELETE_Group_User)
|
||||
|
||||
log.Printf("Starting Access Manager API on port %s\n", strconv.Itoa(Config.ListenPort))
|
||||
|
||||
err = router.Run("0.0.0.0:" + 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 SetupAPISessionStore(config *common.Config) *gin.Engine {
|
||||
secretKey := make([]byte, 256)
|
||||
n, _ := rand.Read(secretKey) // rand Read never returns an error, always crashes on error
|
||||
log.Printf("Generated session secret key of length %d\n", n)
|
||||
func SetupAPISessionStore(router *gin.Engine, config *common.Config) {
|
||||
authKey := make([]byte, 32)
|
||||
encrKey := make([]byte, 32)
|
||||
n, _ := rand.Read(authKey) // rand Read never returns an error, always crashes on error
|
||||
log.Printf("Generated cookie session authentication key of length %d\n", n)
|
||||
n, _ = rand.Read(encrKey) // rand Read never returns an error, always crashes on error
|
||||
log.Printf("Generated cookie session encryption key of length %d\n", n)
|
||||
|
||||
router := gin.Default()
|
||||
store := cookie.NewStore(secretKey)
|
||||
store := cookie.NewStore(authKey, encrKey)
|
||||
store.Options(sessions.Options{
|
||||
Path: config.SessionCookie.Path,
|
||||
HttpOnly: config.SessionCookie.HttpOnly,
|
||||
@@ -550,8 +106,17 @@ func SetupAPISessionStore(config *common.Config) *gin.Engine {
|
||||
router.Use(sessions.Sessions(config.SessionCookieName, store))
|
||||
|
||||
log.Printf("Started cookie store (Name: %s Params: %+v)\n", config.SessionCookieName, config.SessionCookie)
|
||||
}
|
||||
|
||||
return router
|
||||
func GetUserSessionFromContext(c *gin.Context) (*UserSession, 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 GetRealmsFromPVE(config *common.Config) map[string]Realm {
|
||||
|
||||
Reference in New Issue
Block a user