diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index d8778e22..db5111de 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -72,6 +72,23 @@ jobs: - name: Load Docker image run: docker load -i /tmp/docker-image.tar + - name: Cache LLDAP Docker image + uses: actions/cache@v3 + id: lldap-cache + with: + path: /tmp/lldap-image.tar + key: lldap-stable-${{ runner.os }} + + - name: Pull and save LLDAP image + if: steps.lldap-cache.outputs.cache-hit != 'true' + run: | + docker pull nitnelave/lldap:stable + docker save nitnelave/lldap:stable > /tmp/lldap-image.tar + + - name: Load LLDAP image from cache + if: steps.lldap-cache.outputs.cache-hit == 'true' + run: docker load < /tmp/lldap-image.tar + - name: Install frontend dependencies working-directory: ./frontend run: npm ci @@ -81,15 +98,27 @@ jobs: if: steps.playwright-cache.outputs.cache-hit != 'true' run: npx playwright install --with-deps chromium - - name: Run Docker Container with Sqlite DB + - name: Create Docker network + run: docker network create pocket-id-network + + - name: Setup and Configure LLDAP Server + run: | + chmod +x ./scripts/tests/setup-lldap.sh + ./scripts/tests/setup-lldap.sh + + - name: Run Docker Container with Sqlite DB and LDAP run: | docker run -d --name pocket-id-sqlite \ + --network pocket-id-network \ -p 80:80 \ -e APP_ENV=test \ pocket-id:test docker logs -f pocket-id-sqlite &> /tmp/backend.log & + - name: Wait for backend to sync LDAP data + run: sleep 10 + - name: Run Playwright tests working-directory: ./frontend run: npx playwright test @@ -150,6 +179,23 @@ jobs: if: steps.postgres-cache.outputs.cache-hit == 'true' run: docker load < /tmp/postgres-image.tar + - name: Cache LLDAP Docker image + uses: actions/cache@v3 + id: lldap-cache + with: + path: /tmp/lldap-image.tar + key: lldap-stable-${{ runner.os }} + + - name: Pull and save LLDAP image + if: steps.lldap-cache.outputs.cache-hit != 'true' + run: | + docker pull nitnelave/lldap:stable + docker save nitnelave/lldap:stable > /tmp/lldap-image.tar + + - name: Load LLDAP image from cache + if: steps.lldap-cache.outputs.cache-hit == 'true' + run: docker load < /tmp/lldap-image.tar + - name: Download Docker image artifact uses: actions/download-artifact@v4 with: @@ -192,7 +238,12 @@ jobs: sleep 2 done - - name: Run Docker Container with Postgres DB + - name: Setup and Configure LLDAP Server + run: | + chmod +x ./scripts/tests/setup-lldap.sh + ./scripts/tests/setup-lldap.sh + + - name: Run Docker Container with Postgres DB and LDAP run: | docker run -d --name pocket-id-postgres \ --network pocket-id-network \ @@ -204,6 +255,9 @@ jobs: docker logs -f pocket-id-postgres &> /tmp/backend.log & + - name: Wait for backend to sync LDAP data + run: sleep 10 + - name: Run Playwright tests working-directory: ./frontend run: npx playwright test diff --git a/backend/internal/bootstrap/e2etest_router_bootstrap.go b/backend/internal/bootstrap/e2etest_router_bootstrap.go index 226a1b62..bda3360a 100644 --- a/backend/internal/bootstrap/e2etest_router_bootstrap.go +++ b/backend/internal/bootstrap/e2etest_router_bootstrap.go @@ -14,7 +14,7 @@ import ( func init() { registerTestControllers = []func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services){ func(apiGroup *gin.RouterGroup, db *gorm.DB, svc *services) { - testService := service.NewTestService(db, svc.appConfigService, svc.jwtService) + testService := service.NewTestService(db, svc.appConfigService, svc.jwtService, svc.ldapService) controller.NewTestController(apiGroup, testService) }, } diff --git a/backend/internal/controller/e2etest_controller.go b/backend/internal/controller/e2etest_controller.go index 638b7ce5..179285a3 100644 --- a/backend/internal/controller/e2etest_controller.go +++ b/backend/internal/controller/e2etest_controller.go @@ -41,6 +41,16 @@ func (tc *TestController) resetAndSeedHandler(c *gin.Context) { return } + if err := tc.TestService.SetLdapTestConfig(c.Request.Context()); err != nil { + _ = c.Error(err) + return + } + + if err := tc.TestService.SyncLdap(c.Request.Context()); err != nil { + _ = c.Error(err) + return + } + tc.TestService.SetJWTKeys() c.Status(http.StatusNoContent) diff --git a/backend/internal/service/e2etest_service.go b/backend/internal/service/e2etest_service.go index 87ba50fa..dcee2aac 100644 --- a/backend/internal/service/e2etest_service.go +++ b/backend/internal/service/e2etest_service.go @@ -29,15 +29,16 @@ type TestService struct { db *gorm.DB jwtService *JwtService appConfigService *AppConfigService + ldapService *LdapService } -func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService) *TestService { - return &TestService{db: db, appConfigService: appConfigService, jwtService: jwtService} +func NewTestService(db *gorm.DB, appConfigService *AppConfigService, jwtService *JwtService, ldapService *LdapService) *TestService { + return &TestService{db: db, appConfigService: appConfigService, jwtService: jwtService, ldapService: ldapService} } //nolint:gocognit func (s *TestService) SeedDatabase() error { - return s.db.Transaction(func(tx *gorm.DB) error { + err := s.db.Transaction(func(tx *gorm.DB) error { users := []model.User{ { Base: model.Base{ @@ -238,6 +239,12 @@ func (s *TestService) SeedDatabase() error { return nil }) + + if err != nil { + return err + } + + return nil } func (s *TestService) ResetDatabase() error { @@ -349,3 +356,52 @@ func (s *TestService) getCborPublicKey(base64PublicKey string) ([]byte, error) { return cborPublicKey, nil } + +// SyncLdap triggers an LDAP synchronization +func (s *TestService) SyncLdap(ctx context.Context) error { + return s.ldapService.SyncAll(ctx) +} + +// SetLdapTestConfig writes the test LDAP config variables directly to the database. +func (s *TestService) SetLdapTestConfig(ctx context.Context) error { + err := s.db.Transaction(func(tx *gorm.DB) error { + ldapConfigs := map[string]string{ + "ldapUrl": "ldap://lldap:3890", + "ldapBindDn": "uid=admin,ou=people,dc=pocket-id,dc=org", + "ldapBindPassword": "admin_password", + "ldapBase": "dc=pocket-id,dc=org", + "ldapUserSearchFilter": "(objectClass=person)", + "ldapUserGroupSearchFilter": "(objectClass=groupOfNames)", + "ldapSkipCertVerify": "true", + "ldapAttributeUserUniqueIdentifier": "uuid", + "ldapAttributeUserUsername": "uid", + "ldapAttributeUserEmail": "mail", + "ldapAttributeUserFirstName": "givenName", + "ldapAttributeUserLastName": "sn", + "ldapAttributeGroupUniqueIdentifier": "uuid", + "ldapAttributeGroupName": "uid", + "ldapAttributeGroupMember": "member", + "ldapAttributeAdminGroup": "admin_group", + "ldapSoftDeleteUsers": "true", + "ldapEnabled": "true", + } + + for key, value := range ldapConfigs { + configVar := model.AppConfigVariable{Key: key, Value: value} + if err := tx.Create(&configVar).Error; err != nil { + return fmt.Errorf("failed to create config variable '%s': %w", key, err) + } + } + return nil + }) + + if err != nil { + return fmt.Errorf("failed to set LDAP test config: %w", err) + } + + if err := s.appConfigService.LoadDbConfig(ctx); err != nil { + return fmt.Errorf("failed to load app config: %w", err) + } + + return nil +} diff --git a/frontend/tests/application-configuration.spec.ts b/frontend/tests/application-configuration.spec.ts index c4a93bab..7a9f93bb 100644 --- a/frontend/tests/application-configuration.spec.ts +++ b/frontend/tests/application-configuration.spec.ts @@ -53,47 +53,6 @@ test('Update email configuration', async ({ page }) => { await expect(page.getByLabel('API Key Expiration')).toBeChecked(); }); -test('Update LDAP configuration', async ({ page }) => { - await page.goto('/settings/admin/application-configuration'); - - await page.getByRole('button', { name: 'Expand card' }).nth(2).click(); - - await page.getByLabel('LDAP URL').fill('ldap://localhost:389'); - await page.getByLabel('LDAP Bind DN').fill('cn=admin,dc=example,dc=com'); - await page.getByLabel('LDAP Bind Password').fill('password'); - await page.getByLabel('LDAP Base DN').fill('dc=example,dc=com'); - await page.getByLabel('User Search Filter').fill('(objectClass=person)'); - await page.getByLabel('Groups Search Filter').fill('(objectClass=groupOfUniqueNames)'); - await page.getByLabel('User Unique Identifier Attribute').fill('uuid'); - await page.getByLabel('Username Attribute').fill('uid'); - await page.getByLabel('User Mail Attribute').fill('mail'); - await page.getByLabel('User First Name Attribute').fill('givenName'); - await page.getByLabel('User Last Name Attribute').fill('sn'); - await page.getByLabel('Group Unique Identifier Attribute').fill('uuid'); - await page.getByLabel('Group Name Attribute').fill('cn'); - await page.getByLabel('Admin Group Name').fill('admin'); - - await page.getByRole('button', { name: 'Enable' }).click(); - - await expect(page.getByRole('status')).toHaveText('LDAP configuration updated successfully'); - - await page.reload(); - - await expect(page.getByRole('button', { name: 'Disable' })).toBeVisible(); - await expect(page.getByLabel('LDAP URL')).toHaveValue('ldap://localhost:389'); - await expect(page.getByLabel('LDAP Bind DN')).toHaveValue('cn=admin,dc=example,dc=com'); - await expect(page.getByLabel('LDAP Bind Password')).toHaveValue('password'); - await expect(page.getByLabel('LDAP Base DN')).toHaveValue('dc=example,dc=com'); - await page.getByLabel('User Search Filter').fill('(objectClass=person)'); - await page.getByLabel('Groups Search Filter').fill('(objectClass=groupOfUniqueNames)'); - await expect(page.getByLabel('User Unique Identifier Attribute')).toHaveValue('uuid'); - await expect(page.getByLabel('Username Attribute')).toHaveValue('uid'); - await expect(page.getByLabel('User Mail Attribute')).toHaveValue('mail'); - await expect(page.getByLabel('User First Name Attribute')).toHaveValue('givenName'); - await expect(page.getByLabel('User Last Name Attribute')).toHaveValue('sn'); - await expect(page.getByLabel('Admin Group Name')).toHaveValue('admin'); -}); - test('Update application images', async ({ page }) => { await page.goto('/settings/admin/application-configuration'); diff --git a/frontend/tests/ldap.spec.ts b/frontend/tests/ldap.spec.ts new file mode 100644 index 00000000..347fd4ca --- /dev/null +++ b/frontend/tests/ldap.spec.ts @@ -0,0 +1,83 @@ +import test, { expect } from '@playwright/test'; +import { cleanupBackend } from './utils/cleanup.util'; + +test.beforeEach(cleanupBackend); + +test.describe('LDAP Integration', () => { + test('LDAP configuration is working properly', async ({ page }) => { + await page.goto('/settings/admin/application-configuration'); + + await page.getByRole('heading', { name: 'LDAP' }).click(); + + await expect(page.getByRole('button', { name: 'Disable' })).toBeVisible(); + await expect(page.getByLabel('LDAP URL')).toHaveValue(/ldap:\/\/.*/); + await expect(page.getByLabel('LDAP Base DN')).not.toBeEmpty(); + + await expect(page.getByLabel('User Unique Identifier Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('Username Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('User Mail Attribute')).not.toBeEmpty(); + await expect(page.getByLabel('Group Name Attribute')).not.toBeEmpty(); + + const syncButton = page.getByRole('button', { name: 'Sync now' }); + await syncButton.click(); + await expect(page.getByText('LDAP sync finished')).toBeVisible(); + }); + + test('LDAP users are synced into PocketID', async ({ page }) => { + // Navigate to user management + await page.goto('/settings/admin/users'); + + // Verify the LDAP users exist + await expect(page.getByText('testuser1@pocket-id.org')).toBeVisible(); + await expect(page.getByText('testuser2@pocket-id.org')).toBeVisible(); + + // Check LDAP user details + await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + // Verify user source is LDAP + await expect(page.getByText('LDAP').first()).toBeVisible(); + + // Verify essential fields are filled + await expect(page.getByLabel('Username')).not.toBeEmpty(); + await expect(page.getByLabel('Email')).not.toBeEmpty(); + }); + + test('LDAP groups are synced into PocketID', async ({ page }) => { + // Navigate to user groups + await page.goto('/settings/admin/user-groups'); + + // Verify LDAP groups exist + await expect(page.getByRole('cell', { name: 'test_group' }).first()).toBeVisible(); + await expect(page.getByRole('cell', { name: 'admin_group' }).first()).toBeVisible(); + + // Check group details + await page.getByRole('row', { name: 'test_group' }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + // Verify group source is LDAP + await expect(page.getByText('LDAP').first()).toBeVisible(); + }); + + test('LDAP users cannot be modified in PocketID', async ({ page }) => { + // Navigate to LDAP user details + await page.goto('/settings/admin/users'); + await page.getByRole('row', { name: 'testuser1' }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + // Verify key fields are disabled + const usernameInput = page.getByLabel('Username'); + await expect(usernameInput).toBeDisabled(); + }); + + test('LDAP groups cannot be modified in PocketID', async ({ page }) => { + // Navigate to LDAP group details + await page.goto('/settings/admin/user-groups'); + await page.getByRole('row', { name: 'test_group' }).getByRole('button').click(); + await page.getByRole('menuitem', { name: 'Edit' }).click(); + + // Verify key fields are disabled + const nameInput = page.getByLabel('Name', { exact: true }); + await expect(nameInput).toBeDisabled(); + }); +}); diff --git a/scripts/tests/setup-lldap.sh b/scripts/tests/setup-lldap.sh new file mode 100644 index 00000000..c3e32aea --- /dev/null +++ b/scripts/tests/setup-lldap.sh @@ -0,0 +1,100 @@ +#!/bin/bash +set -e + +echo 'deb http://download.opensuse.org/repositories/home:/Masgalor:/LLDAP/xUbuntu_24.04/ /' | sudo tee /etc/apt/sources.list.d/home:Masgalor:LLDAP.list +curl -fsSL https://download.opensuse.org/repositories/home:Masgalor:LLDAP/xUbuntu_24.04/Release.key | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/home_Masgalor_LLDAP.gpg > /dev/null +sudo apt-get update +sudo apt-get install -y lldap-cli lldap-set-password + +echo "Setting up LLDAP container..." + +# Run LLDAP container +docker run -d --name lldap \ + --network pocket-id-network \ + -p 3890:3890 \ + -p 17170:17170 \ + -e LLDAP_JWT_SECRET=secret \ + -e LLDAP_LDAP_USER_PASS=admin_password \ + -e LLDAP_LDAP_BASE_DN="dc=pocket-id,dc=org" \ + nitnelave/lldap:stable + +# Wait for LLDAP to start +for i in {1..15}; do + if curl -s --fail http://localhost:17170/api/healthcheck > /dev/null; then + echo "LLDAP is ready" + break + fi + if [ $i -eq 15 ]; then + echo "LLDAP failed to start in time" + exit 1 + fi + echo "Waiting for LLDAP... ($i/15)" + sleep 3 +done + +echo "LLDAP container setup complete" + +echo "Setting up LLDAP test data..." + +# Configure LLDAP CLI connection via environment variables +export LLDAP_HTTPURL="http://localhost:17170" +export LLDAP_USERNAME="admin" +export LLDAP_PASSWORD="admin_password" + +# Create test users using the user add command +echo "Creating test users..." +lldap-cli user add "testuser1" "testuser1@pocket-id.org" \ + -p "password123" \ + -d "Test User 1" \ + -f "Test" \ + -l "User" + +lldap-cli user add "testuser2" "testuser2@pocket-id.org" \ + -p "password123" \ + -d "Test User 2" \ + -f "Test2" \ + -l "User2" + +# Create test groups +echo "Creating test groups..." +lldap-cli group add "test_group" +sleep 1 +lldap-cli group update set "test_group" "display_name" "test_group" + +lldap-cli group add "admin_group" +sleep 1 +lldap-cli group update set "admin_group" "display_name" "admin_group" + +# Add users to groups with retry logic +echo "Adding users to groups..." +for i in {1..3}; do + echo "Attempt $i to add testuser1 to test_group" + if lldap-cli user group add "testuser1" "test_group"; then + echo "Successfully added testuser1 to test_group" + break + else + echo "Failed to add testuser1 to test_group, retrying in 2 seconds..." + sleep 2 + fi + + if [ $i -eq 3 ]; then + echo "Warning: Could not add testuser1 to test_group after 3 attempts" + fi +done + +for i in {1..3}; do + echo "Attempt $i to add testuser2 to admin_group" + if lldap-cli user group add "testuser2" "admin_group"; then + echo "Successfully added testuser2 to admin_group" + break + else + echo "Failed to add testuser2 to admin_group, retrying in 2 seconds..." + sleep 2 + fi + + if [ $i -eq 3 ]; then + echo "Warning: Could not add testuser2 to admin_group after 3 attempts" + fi +done + +echo "LLDAP test data setup complete" \ No newline at end of file