diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..028acce --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.env.local +.gitignore +.gitlab-ci.yml +docker-compose.yml +Readme.md diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..129ca83 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,15 @@ +stages: + - build + +build-container: + stage: build + image: + name: gcr.io/kaniko-project/executor:v1.9.0-debug + entrypoint: [ "" ] + script: + - /kaniko/executor + --context "${CI_PROJECT_DIR}" + --dockerfile "${CI_PROJECT_DIR}/Dockerfile" + --destination "${CI_REGISTRY_IMAGE}:${CI_COMMIT_TAG}" + only: + - main \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0a2722b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# Start from the official Go image to create a build artifact. +# This is the first stage of a multi-stage build. +FROM golang:1.19 as builder + +# Set the working directory inside the container. +WORKDIR /build + +# Copy go.mod and go.sum to download dependencies. +COPY go.mod . + +# Download Go modules (dependencies). +RUN go mod download + +# Copy the rest of the source code. +COPY . . + +# https://stackoverflow.com/questions/36279253/go-compiled-binary-wont-run-in-an-alpine-docker-container-on-ubuntu-host +ENV CGO_ENABLED=0 + +# Build the Go app as a static binary. +# You might need to add tags or other build arguments depending on your application. +RUN go build -o pocketbase main/main.go + +# Start from a smaller image to make the final image smaller. +FROM alpine:latest + +# Set the working directory inside the container. +WORKDIR /app + +# Copy the statically-linked binary from the builder stage. +COPY --from=builder /build/pocketbase . + +# Expose port 8090 to the outside world. +EXPOSE 8090 + +# Update Path +ENV PATH="/app:${PATH}" + +# Command to run the executable. +CMD ["./pocketbase", "serve", "--http=0.0.0.0:8090", "--dir=/pb_data"] \ No newline at end of file diff --git a/Readme.md b/Readme.md index a3ae65f..6443014 100644 --- a/Readme.md +++ b/Readme.md @@ -42,13 +42,28 @@ Pocketbase selbst bietet folgende Features: - Pocketbase bietet ein JavaScript SDK an, das alle Features von Pocketbase unterstützt. +## Admin UI + +Pocketbase bietet eine Admin UI an, die unter `https:///_/` erreichbar ist. +Der Login hierzu ist im SysPass unter *Pocketbase* zu finden. + +In der Admin UI können Tabellen erstellt, bearbeitet und gelöscht werden. +Außerdem können die API Rules bearbeitet werden, die festlegen, welche +Funktionen für die einzelnen Tabellen und User erlaubt sind. + ## Entwickeln -todo +```bash +git clone ... +cd backend +go run main.go serve # --debug=0 for no debug output +``` ## Installation -todo +Das Projekt kann mittels Docker installiert werden. +Durch die Gitlab CI/CD Pipeline wird bei jedem Push auf `main` ein neues Docker +Image erstellt. Dieses muss dann nur noch auf dem Server deployt werden. ## Projekt Struktur @@ -152,6 +167,12 @@ für diese Collection deaktiviert werden (Username, E-Mail, OAuth2). Mehr dazu unter dem LDAP Login Package. +#### DB Util + +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. @@ -168,7 +189,7 @@ Das Package benötigt folgende ENV Variablen: Dieses Package für der Api die folgende Route hinzu: -`GET /api/ldap/login` +`POST /api/ldap/login` Diese Route nimmt die Parameter `username` und `password` als json Body entgegen: @@ -179,11 +200,9 @@ Diese Route nimmt die Parameter `username` und `password` als json Body entgegen } ``` -Dabei wird das Passwort mit dem LDAP Server abgeglichen und falls -erfolgreich ein Token und das ldap_user Modell zurückgegeben. +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. - -#### Token Refresh - -todo +Diese Response ist identisch mit der standard Pocketbase Authentifizierung (z.B. AuthWithPassword). \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..8927012 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +services: + pocketbase: + image: gitlab.uni-ulm.de:5050/stuve-it/it-tools/backend + #command: + # - "--debug" + container_name: stuve_it_backend + restart: unless-stopped + volumes: + - pb_data:/pb_data + ports: + - 8090:8090 + healthcheck: + test: wget --no-verbose --tries=1 --spider http://localhost:8090/api/health || exit 1 + interval: 5s + timeout: 5s + retries: 5 + environment: + + LDAP_URL: "ldap://dc.stuve.uni-ulm.de" + + LDAP_BIND_DN: "cn=ldapsync,ou=systemaccounts,ou=user,dc=stuve,dc=uni-ulm,dc=de" + LDAP_BIND_PASSWORD: "************" + + LDAP_BASE_DN: "ou=useraccounts,ou=user,dc=stuve,dc=uni-ulm,dc=de" + LDAP_USER_FILTER: "(|(objectCategory=person)(objectClass=user))" + + LDAP_GROUP_FILTER: "(objectClass=group)" + LDAP_GROUP_BASE_DN: "ou=groups,ou=user,dc=stuve,dc=uni-ulm,dc=de" + + LDAP_SYNC_SCHEDULE: "*/1 * * * *" + +volumes: + pb_data: \ No newline at end of file diff --git a/ldapLogin/main.go b/ldapLogin/main.go index 7c7d98a..aae1c4d 100644 --- a/ldapLogin/main.go +++ b/ldapLogin/main.go @@ -8,6 +8,7 @@ import ( "github.com/pocketbase/pocketbase/apis" "github.com/pocketbase/pocketbase/core" "gitlab.uni-ulm.de/stuve-it/it-tools/backend/ldapSync" + "gitlab.uni-ulm.de/stuve-it/it-tools/backend/logger" "os" ) @@ -27,11 +28,16 @@ import ( // if the user is authenticated successfully the endpoint returns and apis.RecordAuthResponse func InitLDAPLogin(app *pocketbase.PocketBase, e *core.ServeEvent) error { - e.Router.GET("/api/ldap/login", func(c echo.Context) 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 { - CN string `json:"cn" form:"cn"` + Username string `json:"username" form:"username"` Password string `json:"password" form:"password"` }{} if err := c.Bind(&data); err != nil { @@ -39,7 +45,7 @@ func InitLDAPLogin(app *pocketbase.PocketBase, e *core.ServeEvent) error { } // step 2: get ldap user by cn from ldapUsers table - record, err := ldapSync.GetLdapUserByCN(app, data.CN) + record, err := ldapSync.GetLdapUserByCN(app, data.Username) // if user does not exist in ldapUsers table return error if err != nil { @@ -57,7 +63,7 @@ func InitLDAPLogin(app *pocketbase.PocketBase, e *core.ServeEvent) error { defer conn.Close() // step 4: bind to ldap server with user credentials from request - err = conn.Bind(data.CN, data.Password) + err = conn.Bind(data.Username, data.Password) if err != nil { // if bind fails return error - invalid credentials return apis.NewBadRequestError("Invalid credentials", err) diff --git a/ldapSync/db.go b/ldapSync/db.go index f0f4f5b..b63a701 100644 --- a/ldapSync/db.go +++ b/ldapSync/db.go @@ -89,6 +89,12 @@ func upsertLDAPUser(app *pocketbase.PocketBase, ldapUser *LDAPUser) error { record = res } else { // if record does not exist, create it record = models.NewRecord(collection) + // refresh token key only if new record is created + // if record is updated, the token key should not be changed because it is used to authenticate the user + // if the token key is changed, the user will be logged out + if err := record.RefreshTokenKey(); err != nil { + return err + } } accountExpires, _ := ldapTimeToUnixTime(ldapUser.accountExpires) @@ -117,8 +123,6 @@ func upsertLDAPUser(app *pocketbase.PocketBase, ldapUser *LDAPUser) error { record.Set("cn", ldapUser.cn) record.Set("memberOf", groups) - record.RefreshTokenKey() - if err := app.Dao().SaveRecord(record); err != nil { return fmt.Errorf("failed to upsert user with dn: %s - %w", ldapUser.dn, err) } @@ -126,18 +130,30 @@ func upsertLDAPUser(app *pocketbase.PocketBase, ldapUser *LDAPUser) error { return nil } +// GetLdapGroupByDN This function returns a record from the ldap groups table by dn. It also expands the memberOf field. func GetLdapGroupByDN(app *pocketbase.PocketBase, dn string) (*models.Record, error) { - return app.Dao().FindFirstRecordByFilter( + record, err := app.Dao().FindFirstRecordByFilter( ldapGroupsTableName, "dn = {:dn}", dbx.Params{"dn": dn}, ) + if err != nil { + return nil, err + } + if errs := app.Dao().ExpandRecord(record, []string{"memberOf"}, nil); len(errs) > 0 { + return nil, fmt.Errorf("ldap group - failed to expand: %v", errs) + } + return record, nil } +// GetLdapUserByCN This function returns a record from the ldap users table by cn. It also expands the memberOf field. func GetLdapUserByCN(app *pocketbase.PocketBase, cn string) (*models.Record, error) { - return app.Dao().FindFirstRecordByFilter( - ldapUsersTableName, - "cn = {:cn}", - dbx.Params{"cn": cn}, - ) + record, err := app.Dao().FindAuthRecordByUsername(ldapUsersTableName, cn) + if err != nil { + return nil, err + } + if errs := app.Dao().ExpandRecord(record, []string{"memberOf"}, nil); len(errs) > 0 { + return nil, fmt.Errorf("ldap user - failed to expand: %v", errs) + } + return record, nil }