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
93 changes: 93 additions & 0 deletions tests-bdd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,99 @@ The extension uses the configuration in `.vscode/settings.json` to locate your f
- After installing the extension, you may need to reload VSCode or the extension for autocompletion to work.
- If autocompletion is not working for specific steps, make sure that the steps are defined properly. Run the tests locally and ensure that no undefined steps are found.

### Multi-Strategy ERS Testing (Claims + LDAP)

The `@multi-strategy-ers` scenarios validate that multi-strategy entity resolution works end-to-end through the full gRPC stack: **SDK → Connect RPC → platform server → ERS → LDAP**.

These tests exist because direct-call Go integration tests (in `service/entityresolution/integration/`) bypass the gRPC/Connect RPC serialization layer. Bugs like structpb coercion (#3645) and pgx driver issues (#3672) only manifest at the serialization boundary — invisible to unit tests but caught by these BDD tests.

#### What's tested

| Scenario | User | LDAP department | Resource | Expected |
|----------|------|----------------|----------|----------|
| 1 | alice | engineering | engineering | PERMIT |
| 2 | bob | marketing | engineering | DENY |
| 3 | charlie | security | security | PERMIT |

Each scenario creates its own namespace, attribute definitions, and subject mappings, then issues an authorization decision request. The ERS resolves the `user_name` entity against LDAP to retrieve department claims, which are then evaluated against subject mapping conditions.

#### Architecture

```text
┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────────────┐ ┌──────────┐
│ SDK │───▶│ Connect RPC │───▶│ Platform │───▶│ Multi-Strategy │───▶│ LDAP │
│ (client) │ │ (gRPC) │ │ (server) │ │ ERS (Claims + │ │ (osixia/ │
│ │◀───│ │◀───│ │◀───│ LDAP providers)│◀───│ openldap)│
└──────────┘ └─────────────┘ └──────────┘ └─────────────────┘ └──────────┘
```

The multi-strategy ERS config uses two providers:
- **claims_passthrough** — passes through JWT claims (e.g. `userName`)
- **ldap_by_username** — looks up the user in LDAP by `uid`, returns `departmentNumber`, `mail`, `uid`

#### Getting started

**Prerequisites:**
- Docker (via Colima or Docker Desktop)
- Go 1.25+
- JDK (`keytool` required for Keycloak truststore generation)

```bash
brew install openjdk
```

**1. Set environment variables** (Colima users):

```bash
export DOCKER_HOST="unix://$HOME/.colima/default/docker.sock"
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock
export TESTCONTAINERS_RYUK_DISABLED=true
export PLATFORM_IMAGE=DEBUG
```

Add `keytool` to PATH if using brew-installed OpenJDK:

```bash
export PATH="/opt/homebrew/opt/openjdk/bin:$PATH"
```

**2. Run the multi-strategy ERS tests:**

```bash
go test ./tests-bdd/ -v --tags=cukes --godog.tags=@multi-strategy-ers --count=1
```

With console logging for debugging:

```bash
CUKES_LOG_HANDLER=console go test ./tests-bdd/ -v --tags=cukes \
--godog.tags=@multi-strategy-ers --godog.format=pretty --count=1
```

**3. Verify output:**
All 3 scenarios should pass (57 steps, 0 failures). You'll see LDAP testcontainer startup, platform service registration, and authorization decision logs.

#### Key files

| File | Purpose |
|------|---------|
| [`features/multi-strategy-ers.feature`](features/multi-strategy-ers.feature) | Gherkin scenarios (3 scenarios, `@stateless`) |
| [`cukes/steps_ldap.go`](cukes/steps_ldap.go) | LDAP testcontainer step definition |
| [`cukes/steps_ers.go`](cukes/steps_ers.go) | Inline ERS configuration step definitions (DocString-based) |

#### How it works

1. **LDAP testcontainer** starts `osixia/openldap:1.5.0` with LDIF fixtures from `service/entityresolution/integration/ldap_test_data/` (8 test users with distinct department values)
2. **Inline ERS configuration** in the feature file's Background defines providers (Claims + LDAP) and mapping strategies with DocString YAML, making the full config visible without referencing external template files
3. **Each scenario** creates a namespace, department attribute (anyOf rule), subject mapping with `.department` selector, and then issues an authorization decision for a `user_name` entity
4. The platform resolves the entity through ERS → LDAP, gets department claims, evaluates subject mappings, and returns PERMIT or DENY

#### Notes

- The feature uses `@stateless` so the platform instance and LDAP container are shared across all scenarios within the feature. Each scenario uses a unique namespace to avoid data conflicts.
- LDAP test data lives in `service/entityresolution/integration/ldap_test_data/` and is shared with the Go integration tests.
- The `--copy-service` flag is used with the LDAP container to avoid macOS `sed -i` compatibility issues with bind mounts.

## TODO
- Improve execution time for platform testing
- Remove keycloak with wiremock/mock or mock
Expand Down
2 changes: 2 additions & 0 deletions tests-bdd/cukes/glue_platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type LocalDevScenarioOptions struct {
InsecureSkipVerifyConn bool
DatabaseName string
PlatformPort int
LDAPPort int
}

func (d *DockerComposeLogger) Printf(format string, v ...interface{}) {
Expand Down Expand Up @@ -135,6 +136,7 @@ func (c *PlatformTestSuiteContext) InitializeScenario(scenarioContext *godog.Sce
KeycloakRealm: trackedScenarioContext.ScenarioOptions.KeycloakRealm,
PlatformEndpoint: trackedScenarioContext.ScenarioOptions.PlatformEndpoint,
InsecureSkipVerifyConn: trackedScenarioContext.ScenarioOptions.InsecureSkipVerifyConn,
LDAPPort: trackedScenarioContext.ScenarioOptions.LDAPPort,
},
TestSuiteContext: c,
}
Expand Down
165 changes: 165 additions & 0 deletions tests-bdd/cukes/steps_ers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package cukes

import (
"context"
"errors"
"fmt"
"text/template"

"github.com/cucumber/godog"
"gopkg.in/yaml.v2"
)

const ersConfigKey = "ers_inline_config"

type ERSInlineConfig struct {
Mode string
FailureStrategy string
Providers map[string]ERSProviderConfig
Strategies []ERSMappingStrategyConfig
}

type ERSProviderConfig struct {
Type string
AutoWireLDAP bool
}

type ERSMappingStrategyConfig struct {
Name string
Provider string
RawYAML string
}

type ERSStepDefinitions struct {
PlatformCukesContext *PlatformTestSuiteContext
PlatformSteps *LocalPlatformStepDefinitions
}

func getERSConfig(sc *PlatformScenarioContext) (*ERSInlineConfig, error) {
obj := sc.GetObject(ersConfigKey)
if obj == nil {
return nil, errors.New("no ERS configuration found; use 'an ERS configuration' step first")
}
cfg, ok := obj.(*ERSInlineConfig)
if !ok {
return nil, errors.New("invalid ERS configuration object")
}
return cfg, nil
}

func (s *ERSStepDefinitions) anERSConfiguration(ctx context.Context, mode string, failureStrategy string) (context.Context, error) {
scenarioContext := GetPlatformScenarioContext(ctx)
cfg := &ERSInlineConfig{
Mode: mode,
FailureStrategy: failureStrategy,
Providers: make(map[string]ERSProviderConfig),
}
scenarioContext.RecordObject(ersConfigKey, cfg)
return ctx, nil
}

func (s *ERSStepDefinitions) anERSProvider(ctx context.Context, name string, providerType string) (context.Context, error) {
scenarioContext := GetPlatformScenarioContext(ctx)
cfg, err := getERSConfig(scenarioContext)
if err != nil {
return ctx, err
}
cfg.Providers[name] = ERSProviderConfig{Type: providerType}
return ctx, nil
}

func (s *ERSStepDefinitions) anERSProviderConnectedToLDAP(ctx context.Context, name string, providerType string) (context.Context, error) {
scenarioContext := GetPlatformScenarioContext(ctx)
cfg, err := getERSConfig(scenarioContext)
if err != nil {
return ctx, err
}
cfg.Providers[name] = ERSProviderConfig{
Type: providerType,
AutoWireLDAP: true,
}
return ctx, nil
}

func (s *ERSStepDefinitions) anERSMappingStrategy(ctx context.Context, name string, provider string, doc *godog.DocString) (context.Context, error) {
scenarioContext := GetPlatformScenarioContext(ctx)
cfg, err := getERSConfig(scenarioContext)
if err != nil {
return ctx, err
}
cfg.Strategies = append(cfg.Strategies, ERSMappingStrategyConfig{
Name: name,
Provider: provider,
RawYAML: doc.Content,
})
return ctx, nil
}

func (s *ERSStepDefinitions) aLocalPlatformWithInlineERSConfiguration(ctx context.Context) (context.Context, error) {
scenarioContext := GetPlatformScenarioContext(ctx)
cfg, err := getERSConfig(scenarioContext)
if err != nil {
return ctx, err
}
kt := template.Must(template.New("kc").Parse(keycloakBaseTemplate))
return s.PlatformSteps.commonLocalPlatform(ctx, &platformStartOptions{
kcProvisionPath: kt,
ersConfig: cfg,
})
}

func buildEntityResolutionConfig(cfg *ERSInlineConfig, hostname string, ldapPort int) (map[string]interface{}, error) {
providers := map[string]interface{}{}
for name, p := range cfg.Providers {
provider := map[string]interface{}{
"type": p.Type,
}
if p.AutoWireLDAP {
provider["connection"] = map[string]interface{}{
"host": hostname,
"port": ldapPort,
"use_tls": false,
"bind_dn": "cn=admin,dc=opentdf,dc=test",
"bind_password": "admin123",
}
} else {
provider["connection"] = map[string]interface{}{}
}
providers[name] = provider
}

var strategies []interface{}
for _, s := range cfg.Strategies {
var rawStrategy map[interface{}]interface{}
if err := yaml.Unmarshal([]byte(s.RawYAML), &rawStrategy); err != nil {
return nil, fmt.Errorf("failed to parse mapping strategy %q YAML: %w", s.Name, err)
}
strategy := map[string]interface{}{
"name": s.Name,
"provider": s.Provider,
}
for k, v := range rawStrategy {
strategy[fmt.Sprintf("%v", k)] = v
}
strategies = append(strategies, strategy)
}

return map[string]interface{}{
"mode": cfg.Mode,
"failure_strategy": cfg.FailureStrategy,
"providers": providers,
"mapping_strategies": strategies,
}, nil
}
Comment thread
khvirtru marked this conversation as resolved.

func RegisterERSStepDefinitions(ctx *godog.ScenarioContext, x *PlatformTestSuiteContext) {
steps := &ERSStepDefinitions{
PlatformCukesContext: x,
PlatformSteps: &LocalPlatformStepDefinitions{PlatformCukesContext: x},
}
ctx.Step(`^an ERS configuration with mode "([^"]*)" and failure strategy "([^"]*)"$`, steps.anERSConfiguration)
ctx.Step(`^an ERS provider "([^"]*)" of type "([^"]*)"$`, steps.anERSProvider)
ctx.Step(`^an ERS provider "([^"]*)" of type "([^"]*)" connected to the LDAP directory$`, steps.anERSProviderConnectedToLDAP)
ctx.Step(`^an ERS mapping strategy "([^"]*)" using provider "([^"]*)"$`, steps.anERSMappingStrategy)
ctx.Step(`^a local platform with inline ERS configuration$`, steps.aLocalPlatformWithInlineERSConfiguration)
}
Loading
Loading