Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .github/workflows/subsystem.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ on:
branches: [main, 'release/v*']

jobs:
auth-subsystem:
uses: dcm-project/shared-workflows/.github/workflows/black-box.yaml@main
with:
up-target: auth-subsystem-test-up
test-target: auth-subsystem-test
down-target: auth-subsystem-test-down
images: >-
registry.access.redhat.com/ubi9/go-toolset:1.25.5
registry.access.redhat.com/ubi9/ubi-minimal:latest
quay.io/sclorg/postgresql-16-c9s:latest
quay.io/keycloak/keycloak:26.0
Comment thread
qodo-code-review[bot] marked this conversation as resolved.

policy-subsystem:
uses: dcm-project/shared-workflows/.github/workflows/black-box.yaml@main
with:
Expand Down
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export COMPOSE_PROJECT_NAME
CONTAINER_IMAGE_NAME ?= quay.io/dcm-project/$(BINARY_NAME)
CONTAINER_IMAGE_TAG ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)

include make/auth.mk
include make/catalog.mk
include make/placement.mk
include make/policy.mk
Expand Down
15 changes: 15 additions & 0 deletions make/auth.mk
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Auth domain (subsystem tests).
AUTH_DOMAIN := auth

AUTH_COMPOSE = COMPOSE_PROJECT_NAME=auth-subsystem $(COMPOSE) -f test/subsystem/$(AUTH_DOMAIN)/docker-compose.yaml

auth-subsystem-test-up:
$(AUTH_COMPOSE) up -d --build

auth-subsystem-test-down:
$(AUTH_COMPOSE) down -v

auth-subsystem-test:
$(GINKGO) $(GINKGO_FLAGS) -tags=subsystem ./test/subsystem/$(AUTH_DOMAIN)

.PHONY: auth-subsystem-test-up auth-subsystem-test-down auth-subsystem-test
28 changes: 28 additions & 0 deletions test/subsystem/auth/admin_seed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build subsystem

package subsystem_test

import (
"net/http"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Admin actor seeding", func() {
It("seeds an active admin actor on startup", func() {
resp := doRequest(http.MethodGet, "/catalog-items",
withProxySecret(proxySecret, adminSubject))
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))

_, username, status := getActorByExternalID(adminSubject)
Expect(username).To(Equal("admin"))
Expect(status).To(Equal("active"))
})

It("binds the admin identity to the configured subject", func() {
count := countActorsByExternalID(adminSubject)
Expect(count).To(Equal(1))
})
})
78 changes: 78 additions & 0 deletions test/subsystem/auth/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Auth subsystem test stack. Runs with AUTH_DISABLED=false to exercise
# the full authentication middleware, JIT provisioning, and OIDC flow.

services:
postgres:
image: quay.io/sclorg/postgresql-16-c9s:latest
environment:
POSTGRESQL_DATABASE: auth_test
POSTGRESQL_USER: ${POSTGRES_USER:-test_user}
POSTGRESQL_PASSWORD: ${POSTGRES_PASSWORD:-test_password}
ports:
- "25432:5432"
volumes:
- postgres_data:/var/lib/pgsql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-test_user} -d auth_test"]
interval: 5s
timeout: 5s
retries: 5

keycloak:
image: quay.io/keycloak/keycloak:26.0
entrypoint: ["/bin/sh", "-c"]
command:
- "mkdir -p /opt/keycloak/data/import && content=$$(cat /opt/keycloak/data/realm-template.json) && printf '%s' \"$${content//REPLACE_DCM_DEV_PASSWORD/$$DCM_DEV_USER_PASSWORD}\" > /opt/keycloak/data/import/realm-export.json && exec /opt/keycloak/bin/kc.sh start-dev --import-realm"
environment:
KC_HEALTH_ENABLED: "true"
KC_HOSTNAME: "http://keycloak:8080"
KC_HOSTNAME_STRICT: "false"
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: ${KEYCLOAK_ADMIN_PASSWORD:-admin}
DCM_DEV_USER_PASSWORD: ${DCM_DEV_USER_PASSWORD:-admin}
ports:
- "28180:8080"
volumes:
- ../../../deploy/keycloak/realm-export.json:/opt/keycloak/data/realm-template.json:ro,z
healthcheck:
test: ["CMD-SHELL", "exec 3<>/dev/tcp/localhost/9000 && echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && cat <&3 | grep -q '\"status\": \"UP\"'"]
interval: 10s
timeout: 5s
retries: 15
start_period: 30s

control-plane:
build:
context: ../../..
dockerfile: Containerfile
environment:
BIND_ADDRESS: "0.0.0.0:8080"
DB_TYPE: pgsql
DB_HOST: postgres
DB_PORT: "5432"
DB_NAME: auth_test
DB_USER: ${POSTGRES_USER:-test_user}
DB_PASSWORD: ${POSTGRES_PASSWORD:-test_password}
NATS_DISABLED: "true"
AUTH_DISABLED: "false"
AUTH_PROXY_SECRET: "test-proxy-secret"
AUTH_ISSUER_URL: "http://keycloak:8080/realms/dcm"
AUTH_JWT_AUDIENCE: "dcm-api"
AUTH_CACHE_TTL: "2s"
DCM_ADMIN_SUBJECT: "56deb662-4820-5d83-b828-f4beb11a5fa7"
LOG_LEVEL: debug
ports:
- "28080:8080"
depends_on:
postgres:
condition: service_healthy
keycloak:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/api/v1alpha1/health"]
interval: 5s
timeout: 5s
retries: 15

volumes:
postgres_data: {}
53 changes: 53 additions & 0 deletions test/subsystem/auth/error_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:build subsystem

package subsystem_test

import (
"net/http"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("RFC 7807 error response format", func() {
It("returns well-formed problem+json on 401", func() {
resp := doRequest(http.MethodGet, "/catalog-items")
Expect(resp.StatusCode).To(Equal(http.StatusUnauthorized))
Expect(resp.Header.Get("Content-Type")).To(Equal("application/problem+json"))
Expect(resp.Header.Get("WWW-Authenticate")).To(Equal("Bearer"))

problem := readProblemResponse(resp)
Expect(problem.Type).To(Equal("UNAUTHENTICATED"))
Expect(problem.Status).To(Equal(401))
Expect(problem.Title).To(Equal("Unauthorized"))
Expect(problem.Detail).NotTo(BeEmpty())
})

It("returns well-formed problem+json on 403", func() {
adminToken := getKeycloakAdminToken()
username := uniqueUsername()
kcUserID := createKeycloakUser(adminToken, username, "testpass")
defer deleteKeycloakUser(adminToken, kcUserID)

resp := doRequest(http.MethodGet, "/catalog-items",
withProxySecret(proxySecret, kcUserID),
withPreferredUsername(username))
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))

updateActorStatus(kcUserID, "suspended")
time.Sleep(3 * time.Second)

resp2 := doRequest(http.MethodGet, "/catalog-items",
withProxySecret(proxySecret, kcUserID))
Expect(resp2.StatusCode).To(Equal(http.StatusForbidden))
Expect(resp2.Header.Get("Content-Type")).To(Equal("application/problem+json"))

problem := readProblemResponse(resp2)
Expect(problem.Type).To(Equal("PERMISSION_DENIED"))
Expect(problem.Status).To(Equal(403))
Expect(problem.Title).To(Equal("Forbidden"))
Expect(problem.Detail).To(Equal("account suspended"))
})
})
35 changes: 35 additions & 0 deletions test/subsystem/auth/health_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
//go:build subsystem

package subsystem_test

import (
"io"
"net/http"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Health endpoint bypass", func() {
It("returns 200 with no auth headers", func() {
resp := doRequest(http.MethodGet, "/health")
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))

body, err := io.ReadAll(resp.Body)
Expect(err).NotTo(HaveOccurred())
Expect(string(body)).To(ContainSubstring(`"status":"ok"`))
})

It("returns 200 even with an invalid Bearer token", func() {
resp := doRequest(http.MethodGet, "/health", withBearerToken("garbage-token"))
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))
})

It("returns 200 even with a wrong proxy secret", func() {
resp := doRequest(http.MethodGet, "/health", withProxySecret("wrong-secret", "any-subject"))
defer resp.Body.Close()
Expect(resp.StatusCode).To(Equal(http.StatusOK))
})
})
Loading