feat(ldapApi): LDAP Search Api
Build and Push Docker image / build-and-push (push) Has been cancelled
Details
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:
parent
2861b3830d
commit
3bc4a2740c
116
Readme.md
116
Readme.md
|
@ -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)`)
|
||||
|
@ -242,38 +318,4 @@ 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).
|
||||
die Beziehungen zu anderen Tabellen laden (`memberOf`).
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue