feat(ldapApi): LDAP Search Api
Build and Push Docker image / build-and-push (push) Has been cancelled Details

Implemented new api endpoint (/api/ldap/search) to search in the ldap database
This commit is contained in:
Valentin Kolb 2024-03-27 17:57:58 +01:00
parent 2861b3830d
commit 3bc4a2740c
5 changed files with 266 additions and 118 deletions

114
Readme.md
View File

@ -84,8 +84,8 @@ Die einzelnen custom go Packages sind weiter unten beschrieben.
│ ├── models.go # Structs für LDAP User und Group
│ ├── tables.go # Erstellt die benötigten Tabellen in der Datenbank (Users, Groups, Logs)
│ └── ldapSync.go # Sync Funktionen (Sync Users, Groups, ...)
├── ldapLogin # LDAP Login Package
│ └── main.go # Login Funktionen (Login Route)
├── ldapApi # LDAP API Package
│ └── main.go # Api Route
└── qrApi # QR API Package
└── main.go # Api Route
```
@ -141,6 +141,81 @@ Parameter als json entgegennimmt:
curl -X GET https://it.stuve.uni-ulm.de/api/qr/v1 -d '{"data": "https://stuve.uni-ulm.de"}'
```
### LDAP API
Im LDAP API Package sind zwei API Routen implementiert:
- LDAP Login: `POST /api/ldap/login`
- LDAP Search: `GET /api/ldap/search`
#### LDAP Login
Dieses Package implementiert eine LDAP Authentifizierung, die Pocketbase um eine LDAP Authentifizierung erweitert.
Das Package bietet die Route `POST /api/ldap/login` an, welche die folgenden
Parameter als json entgegennimmt:
| Parameter | Datentyp | Beschreibung |
|------------|----------|-------------------------------------------------------|
| `username` | string | Der Benutzername des LDAP Users (`cn`) (**required**) |
| `password` | string | Das Passwort des LDAP Users (**required**) |
##### Konfiguration
Das Package benötigt folgende ENV Variablen:
- `LDAP_URL`: LDAP Server URL (e.g. `ldap://ldap.stuve.uni-ulm.de`)
##### Mechanismus
Das Passwort wird mit dem LDAP Server abgeglichen und nach einer
erfolgreichen Authentifizierung ein Token und das ldap_user Modell
zurückgegeben. Die `memberOf` Beziehungen werden dabei automatisch geladen.
Das Passwort wird nicht in der Datenbank gespeichert.
Diese Response ist identisch mit der standard Pocketbase Authentifizierung (z.B. AuthWithPassword).
##### Beispiel
```bash
curl -X POST https://it.stuve.uni-ulm.de/api/ldap/login -d '{"username": "vorname.nachname", "password": "*******"}'
```
#### LDAP Search
Dieses Package implementiert eine LDAP Suche, die Pocketbase um eine LDAP Suche erweitert.
Damit kann per HTTP Request die LDAP Datenbank durchsucht werden.
Das Package bietet die Route `GET /api/ldap/search` an, welche die folgenden
Parameter als json entgegennimmt:
| Parameter | Datentyp | Beschreibung |
|--------------|----------|-------------------------------------------------------------------------------------------------------------|
| `baseDN` | string | Die Base DN, in der gesucht werden soll (**required**) |
| `filter` | string | Der Filter, nach dem gesucht werden soll (**required**) |
| `attributes` | []string | Die Attribute, die zurückgegeben werden sollen, bei einem leeren Array werden alle Attribute zurückgegeben. |
##### Konfiguration
Das Package benötigt folgende ENV Variablen:
- `LDAP_URL`: LDAP Server URL (e.g. `ldap://ldap.stuve.uni-ulm.de`)
- `LDAP_BIND_DN`: LDAP Bind DN (zum Lesen der LDAP Datenbank, admin user)
- `LDAP_ADMIN_GROUP_DN`: LDAP Admin Group DN (Gruppe, die Admin Rechte hat)
##### Mechanismus
Die Route führt eine LDAP Suche durch und gibt die gefundenen LDAP
Objekte zurück. Dabei wird die Suche mit dem `LDAP_BIND_DN` durchgeführt.
Es wird überprüft, ob der User der das GET Request schickt in der `LDAP_ADMIN_GROUP_DN` Gruppe ist.
##### Beispiel
```bash
curl -X GET https://it.stuve.uni-ulm.de/api/ldap/search -d '{"baseDN": "ou=useraccounts,ou=user,dc=stuve,dc=uni-ulm,dc=de", "filter": "(cn=vorname.nachname)"}'
```
### LDAP Sync
Dieses Package ist das umfangreichste Package und synchronisiert
@ -189,6 +264,7 @@ Das Package benötigt folgende ENV Variablen:
- `LDAP_USER_BASE_DN`: LDAP User Base DN (hier werden alle User gesucht,
e.g. `ou=useraccounts,ou=user,dc=stuve,dc=uni-ulm,dc=de`)
- `LDAP_USER_FILTER`: LDAP User Filter (e.g. `(|(objectCategory=person)(objectClass=user))`)
- `LDAP_ADMIN_GROUP_DN`: LDAP Admin Group DN (Gruppe, die Admin Rechte hat)
- `LDAP_GROUP_BASE_DN`: LDAP Group Base DN (hier werden alle Gruppen gesucht,
e.g. `ou=groups,ou=user,dc=stuve,dc=uni-ulm,dc=de`)
- `LDAP_GROUP_FILTER`: LDAP Group Filter (e.g. `(objectClass=group)`)
@ -243,37 +319,3 @@ Mehr dazu unter dem LDAP Login Package.
In der `db.go` Datei sind alle Datenbank Funktionen implementiert.
Hier gibt es Methoden um User oder Groups zu finden, die dabei automatisch
die Beziehungen zu anderen Tabellen laden (`memberOf`).
### LDAP Login
Dieses Package ist für die Authentifizierung zuständig.
Es implementiert eine LDAP Authentifizierung, die Pocketbase
um eine LDAP Authentifizierung erweitert.
#### Konfiguration
Das Package benötigt folgende ENV Variablen:
- `LDAP_URL`: LDAP Server URL (e.g. `ldap://ldap.stuve.uni-ulm.de`)
#### Mechanismus
Dieses Package für der Api die folgende Route hinzu:
`POST /api/ldap/login`
Diese Route nimmt die Parameter `username` und `password` als json Body entgegen:
```json
{
"username": "username",
"password": "password"
}
```
Das Passwort wird mit dem LDAP Server abgeglichen und nach einer
erfolgreichen Authentifizierung ein Token und das ldap_user Modell
zurückgegeben. Die `memberOf` Beziehungen werden dabei automatisch geladen.
Das Passwort wird nicht in der Datenbank gespeichert.
Diese Response ist identisch mit der standard Pocketbase Authentifizierung (z.B. AuthWithPassword).

183
ldapApi/main.go Normal file
View File

@ -0,0 +1,183 @@
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
}

View File

@ -1,77 +0,0 @@
package ldapLogin
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"
"os"
)
// InitLDAPLogin initializes ldap login endpoint
//
// this endpoint is used to authenticate users against ldap server
// it adds the following endpoint to the app:
// GET /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) error {
// add endpoint to app
logger.LogInfoF("Adding LDAP Login Endpoint")
e.Router.POST("/api/ldap/login", func(c echo.Context) error {
logger.LogInfoF("LDAP Login")
// 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))
return nil
}

View File

@ -48,7 +48,7 @@ func syncLdapGroups(app *pocketbase.PocketBase, ldapClient *ldap.Conn) SyncResul
// Create a search request for groups
groupsSearchRequest := ldap.NewSearchRequest(
os.Getenv("LDAP_GROUP_BASE_DN"), // The base dn to search
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false,
os.Getenv("LDAP_GROUP_FILTER"), // The filter to apply
[]string{"description", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"},
nil,
@ -136,7 +136,7 @@ func syncLdapUsers(app *pocketbase.PocketBase, ldapClient *ldap.Conn) SyncResult
// Create a search request for users
userSearchRequest := ldap.NewSearchRequest(
os.Getenv("LDAP_BASE_DN"), // The base dn to search
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false,
os.Getenv("LDAP_USER_FILTER"), // The filter to apply
[]string{"givenName", "sn", "accountExpires", "mail", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"},
nil,

View File

@ -1,7 +1,7 @@
package main
import (
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/ldapLogin"
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/ldapApi"
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/ldapSync"
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/logger"
"git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/qrApi"
@ -33,7 +33,7 @@ func main() {
// setup ldap login
app.OnBeforeServe().Add(func(e *core.ServeEvent) error {
return ldapLogin.InitLDAPLogin(app, e)
return ldapApi.InitLdapApi(app, e)
})
// setup qr api