stuve-it-backend/ldapApi/main.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
}