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

116
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 │ ├── models.go # Structs für LDAP User und Group
│ ├── tables.go # Erstellt die benötigten Tabellen in der Datenbank (Users, Groups, Logs) │ ├── tables.go # Erstellt die benötigten Tabellen in der Datenbank (Users, Groups, Logs)
│ └── ldapSync.go # Sync Funktionen (Sync Users, Groups, ...) │ └── ldapSync.go # Sync Funktionen (Sync Users, Groups, ...)
├── ldapLogin # LDAP Login Package ├── ldapApi # LDAP API Package
│ └── main.go # Login Funktionen (Login Route) │ └── main.go # Api Route
└── qrApi # QR API Package └── qrApi # QR API Package
└── main.go # Api Route └── 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"}' 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 ### LDAP Sync
Dieses Package ist das umfangreichste Package und synchronisiert 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, - `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`) 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_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, - `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`) e.g. `ou=groups,ou=user,dc=stuve,dc=uni-ulm,dc=de`)
- `LDAP_GROUP_FILTER`: LDAP Group Filter (e.g. `(objectClass=group)`) - `LDAP_GROUP_FILTER`: LDAP Group Filter (e.g. `(objectClass=group)`)
@ -242,38 +318,4 @@ Mehr dazu unter dem LDAP Login Package.
In der `db.go` Datei sind alle Datenbank Funktionen implementiert. In der `db.go` Datei sind alle Datenbank Funktionen implementiert.
Hier gibt es Methoden um User oder Groups zu finden, die dabei automatisch Hier gibt es Methoden um User oder Groups zu finden, die dabei automatisch
die Beziehungen zu anderen Tabellen laden (`memberOf`). 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 // Create a search request for groups
groupsSearchRequest := ldap.NewSearchRequest( groupsSearchRequest := ldap.NewSearchRequest(
os.Getenv("LDAP_GROUP_BASE_DN"), // The base dn to search 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 os.Getenv("LDAP_GROUP_FILTER"), // The filter to apply
[]string{"description", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"}, []string{"description", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"},
nil, nil,
@ -136,7 +136,7 @@ func syncLdapUsers(app *pocketbase.PocketBase, ldapClient *ldap.Conn) SyncResult
// Create a search request for users // Create a search request for users
userSearchRequest := ldap.NewSearchRequest( userSearchRequest := ldap.NewSearchRequest(
os.Getenv("LDAP_BASE_DN"), // The base dn to search 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 os.Getenv("LDAP_USER_FILTER"), // The filter to apply
[]string{"givenName", "sn", "accountExpires", "mail", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"}, []string{"givenName", "sn", "accountExpires", "mail", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"},
nil, nil,

View File

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