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 }