184 lines
5.2 KiB
Go
184 lines
5.2 KiB
Go
package ldapApi
|
|
|
|
import (
|
|
"fmt"
|
|
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/ldapSync"
|
|
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/logger"
|
|
"github.com/go-ldap/ldap/v3"
|
|
"github.com/labstack/echo/v5"
|
|
"github.com/pocketbase/pocketbase"
|
|
"github.com/pocketbase/pocketbase/apis"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/models"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// UserIsInAdminGroup checks if the user is in the admin group
|
|
//
|
|
// the admin group is defined by the environment variable LDAP_ADMIN_GROUP_DN
|
|
//
|
|
// if the user is in the admin group the function returns nil else an apis.NewUnauthorizedError
|
|
func UserIsInAdminGroup(app *pocketbase.PocketBase, c echo.Context) error {
|
|
|
|
// get current user record
|
|
record, _ := c.Get(apis.ContextAuthRecordKey).(*models.Record)
|
|
if record == nil {
|
|
return apis.NewUnauthorizedError("Unauthorized", nil)
|
|
}
|
|
|
|
// expand the record to get all groups
|
|
if errs := app.Dao().ExpandRecord(record, []string{"memberOf"}, nil); len(errs) > 0 {
|
|
return apis.NewApiError(500, "Error expanding groups", nil)
|
|
}
|
|
|
|
// get all groups
|
|
groups := record.ExpandedAll("memberOf")
|
|
|
|
// check if user is in admin group
|
|
for _, group := range groups {
|
|
if strings.ToLower(group.Get("dn").(string)) == strings.ToLower(os.Getenv("LDAP_ADMIN_GROUP_DN")) {
|
|
// return no error if user is in admin group
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// return error if user is not in admin group
|
|
return apis.NewUnauthorizedError("Unauthorized: user must be in admin group for ldap search", nil)
|
|
}
|
|
|
|
// initLdapLogin
|
|
//
|
|
// this endpoint is used to authenticate users against ldap server
|
|
// it adds the following endpoint to the app:
|
|
// POST /api/ldap/login
|
|
//
|
|
// the endpoint expects the following request data:
|
|
//
|
|
// {
|
|
// "cn": "user common name",
|
|
// "password": "user password"
|
|
// }
|
|
//
|
|
// if the user is authenticated successfully the endpoint returns and apis.RecordAuthResponse
|
|
func initLdapLogin(app *pocketbase.PocketBase, e *core.ServeEvent) {
|
|
logger.LogInfoF("Adding LDAP Login Endpoint")
|
|
e.Router.POST("/api/ldap/login", func(c echo.Context) error {
|
|
|
|
// step 1: get data from request
|
|
data := struct {
|
|
Username string `json:"username" form:"username"`
|
|
Password string `json:"password" form:"password"`
|
|
}{}
|
|
if err := c.Bind(&data); err != nil {
|
|
return apis.NewBadRequestError("Failed to read request data", err)
|
|
}
|
|
|
|
// step 2: get ldap user by cn from ldapUsers table
|
|
record, err := ldapSync.GetLdapUserByCN(app, data.Username)
|
|
|
|
// if user does not exist in ldapUsers table return error
|
|
if err != nil {
|
|
return apis.NewBadRequestError("Invalid credentials", err)
|
|
}
|
|
|
|
// step 3: connect to ldap server
|
|
conn, err := ldap.DialURL(os.Getenv("LDAP_URL"))
|
|
if err != nil {
|
|
return apis.NewBadRequestError(
|
|
"Failed to read request data",
|
|
fmt.Errorf("unable to connect to ldap server - %s", err),
|
|
)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// step 4: bind to ldap server with user credentials from request
|
|
err = conn.Bind(data.Username, data.Password)
|
|
if err != nil {
|
|
// if bind fails return error - invalid credentials
|
|
return apis.NewBadRequestError("Invalid credentials", err)
|
|
}
|
|
|
|
// return auth response
|
|
return apis.RecordAuthResponse(app, c, record, nil)
|
|
}, apis.ActivityLogger(app))
|
|
}
|
|
|
|
// initLdapSearch
|
|
//
|
|
// this endpoint is used to search the ldap server
|
|
// it adds the following endpoint to the app:
|
|
// GET /api/ldap/search
|
|
//
|
|
// the endpoint expects the following request data:
|
|
//
|
|
// {
|
|
// "baseDN": "base dn to search",
|
|
// "filter": "filter to apply",
|
|
// "attributes": ["attributes to return"] (empty array returns all attributes)
|
|
// }
|
|
func initLdapSearch(app *pocketbase.PocketBase, e *core.ServeEvent) {
|
|
logger.LogInfoF("Adding LDAP Search Endpoint")
|
|
|
|
e.Router.GET("/api/ldap/search", func(c echo.Context) error {
|
|
|
|
// check if user is in admin group
|
|
if err := UserIsInAdminGroup(app, c); err != nil {
|
|
return err
|
|
}
|
|
|
|
// get data from request
|
|
data := struct {
|
|
BaseDN string `json:"baseDN"`
|
|
Filter string `json:"filter"`
|
|
Attributes []string `json:"attributes"`
|
|
}{}
|
|
if err := c.Bind(&data); err != nil {
|
|
return apis.NewBadRequestError("Failed to read request data", err)
|
|
}
|
|
|
|
// Connect to the LDAP server
|
|
|
|
conn, err := ldap.DialURL(os.Getenv("LDAP_URL"))
|
|
if err != nil {
|
|
return apis.NewBadRequestError("unable to connect to ldap server", err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
// Bind to the server
|
|
err = conn.Bind(os.Getenv("LDAP_BIND_DN"), os.Getenv("LDAP_BIND_PASSWORD"))
|
|
if err != nil {
|
|
return apis.NewBadRequestError("unable to bind to ldap server", err)
|
|
}
|
|
|
|
// Create a search request for groups
|
|
searchRequest := ldap.NewSearchRequest(
|
|
data.BaseDN, // The base dn to search
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false,
|
|
data.Filter, // The filter to apply
|
|
data.Attributes,
|
|
nil,
|
|
)
|
|
|
|
searchResult, err := conn.Search(searchRequest)
|
|
if err != nil {
|
|
return apis.NewBadRequestError("ldap search failed", err)
|
|
}
|
|
|
|
return c.JSON(200, searchResult)
|
|
}, apis.ActivityLogger(app))
|
|
|
|
}
|
|
|
|
// InitLdapApi initializes ldap api endpoints
|
|
func InitLdapApi(app *pocketbase.PocketBase, e *core.ServeEvent) error {
|
|
|
|
logger.LogInfoF("Initializing ldap api")
|
|
|
|
initLdapLogin(app, e)
|
|
initLdapSearch(app, e)
|
|
|
|
return nil
|
|
|
|
}
|