Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[skunkworks] Auditing contract testing #1589

Merged
merged 4 commits into from
Jun 4, 2024
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
5 changes: 5 additions & 0 deletions .github/workflows/cloud-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ jobs:
with:
forked: ${{ inputs.forked }}

contract-tests:
needs: allowed
uses: ./.github/workflows/test-contract.yml
secrets: inherit

e2e-tests:
needs: allowed
uses: ./.github/workflows/test-e2e.yml
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/test-contract.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Contract Tests

on:
workflow_call:
workflow_dispatch:

jobs:
contract:
name: Contract Tests
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
with:
ref: ${{github.event.pull_request.head.sha}}

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: "${{ github.workspace }}/go.mod"
cache: false

- name: Create k8s Kind Cluster
uses: helm/kind-action@v1.10.0
with:
version: v0.22.0
config: test/helper/e2e/config/kind.yaml
node_image: kindest/node:v1.29.2

- name: Run Contract Testing
env:
AKO_CONTRACT_TEST: 1
MCLI_OPS_MANAGER_URL: https://cloud-qa.mongodb.com
MCLI_ORG_ID: ${{ secrets.ATLAS_ORG_ID }}
MCLI_PUBLIC_API_KEY: ${{ secrets.ATLAS_PUBLIC_KEY }}
MCLI_PRIVATE_API_KEY: ${{ secrets.ATLAS_PRIVATE_KEY }}
run: make contract-tests
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -562,3 +562,8 @@ upload-sbom-to-silk: ## Upload a give SBOM (lite) to Silk
@ARTIFACTORY_USERNAME=$(ARTIFACTORY_USERNAME) ARTIFACTORY_PASSWORD=$(ARTIFACTORY_PASSWORD) \
SILK_CLIENT_ID=$(SILK_CLIENT_ID) SILK_CLIENT_SECRET=$(SILK_CLIENT_SECRET) \
SILK_ASSET_GROUP=$(SILK_ASSET_GROUP) ./scripts/upload-to-silk.sh $(SBOM_JSON_FILE)

.PHONY: contract-tests
contract-tests: ## Run contract tests
go clean -testcache
AKO_CONTRACT_TEST=1 go test -v -race -cover ./test/contract/...
54 changes: 54 additions & 0 deletions internal/translation/audit/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package audit

import (
"context"
"fmt"

"go.mongodb.org/atlas-sdk/v20231115008/admin"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/types"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
)

// AuditLogService is the interface exposed by this translation layer over
// the Atlas AuditLog
type AuditLogService interface {
Get(ctx context.Context, projectID string) (*AuditConfig, error)
Set(ctx context.Context, projectID string, auditing *AuditConfig) error
}

// AuditLog is the default implementation of the AuditLogService using the Atlas SDK
type AuditLog struct {
auditAPI admin.AuditingApi
}

// NewAuditLogService creates an AuditLog from credentials and the atlas provider
func NewAuditLogService(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*AuditLog, error) {
client, err := translation.NewVersionedClient(ctx, provider, secretRef, log)
if err != nil {
return nil, err
}
return NewAuditLog(client.AuditingApi), nil
}

// NewAuditLog wraps the SDK AuditingApi as an AuditLog
func NewAuditLog(api admin.AuditingApi) *AuditLog {
return &AuditLog{auditAPI: api}
}

// Get an Atlas Project audit log configuration
func (s *AuditLog) Get(ctx context.Context, projectID string) (*AuditConfig, error) {
auditLog, _, err := s.auditAPI.GetAuditingConfiguration(ctx, projectID).Execute()
if err != nil {
return nil, fmt.Errorf("failed to get audit log from Atlas: %w", err)
}
return fromAtlas(auditLog)
}

// Set an Atlas Project audit log configuration
func (s *AuditLog) Set(ctx context.Context, projectID string, auditing *AuditConfig) error {
_, _, err := s.auditAPI.UpdateAuditingConfiguration(ctx, projectID, toAtlas(auditing)).Execute()
return err
}
57 changes: 57 additions & 0 deletions internal/translation/audit/conversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package audit

import (
"fmt"

"go.mongodb.org/atlas-sdk/v20231115008/admin"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
)

type AuditingConfigType string

const (
None AuditingConfigType = "NONE"
FilterBuilder AuditingConfigType = "FILTER_BUILDER"
FilterJSON AuditingConfigType = "FILTER_JSON"
)

// AuditConfig represents the Atlas Project audit log config
type AuditConfig struct {
Enabled bool
AuditAuthorizationSuccess bool
ConfigurationType AuditingConfigType
AuditFilter string
}

func toAtlas(auditing *AuditConfig) *admin.AuditLog {
return &admin.AuditLog{
Enabled: pointer.MakePtr(auditing.Enabled),
AuditAuthorizationSuccess: pointer.MakePtr(auditing.AuditAuthorizationSuccess),
AuditFilter: pointer.MakePtr(auditing.AuditFilter),
// ConfigurationType is not set on the PATCH operation to Atlas
}
}

func fromAtlas(auditLog *admin.AuditLog) (*AuditConfig, error) {
cfgType, err := configTypeFromAtlas(auditLog.ConfigurationType)
if err != nil {
return nil, err
}
return &AuditConfig{
Enabled: pointer.GetOrDefault(auditLog.Enabled, false),
AuditAuthorizationSuccess: pointer.GetOrDefault(auditLog.AuditAuthorizationSuccess, false),
ConfigurationType: cfgType,
AuditFilter: pointer.GetOrDefault(auditLog.AuditFilter, ""),
}, nil
}

func configTypeFromAtlas(configType *string) (AuditingConfigType, error) {
ct := pointer.GetOrDefault(configType, string(None))
switch ct {
case string(None), string(FilterBuilder), string(FilterJSON):
return AuditingConfigType(ct), nil
default:
return AuditingConfigType(ct), fmt.Errorf("unsupported Auditing Config type %q", ct)
}
}
20 changes: 20 additions & 0 deletions internal/translation/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package translation

import (
"context"
"fmt"

"go.mongodb.org/atlas-sdk/v20231115008/admin"
"go.uber.org/zap"
"k8s.io/apimachinery/pkg/types"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
)

func NewVersionedClient(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*admin.APIClient, error) {
s-urbaniak marked this conversation as resolved.
Show resolved Hide resolved
apiClient, _, err := provider.SdkClient(ctx, secretRef, log)
if err != nil {
return nil, fmt.Errorf("failed to instantiate Versioned Atlas client: %w", err)
}
return apiClient, nil
}
147 changes: 147 additions & 0 deletions test/contract/audit/audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package audit

import (
"context"
_ "embed"
"log"
"os"
"testing"
"time"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/audit"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control"
"github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/launcher"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/test/contract"
)

//go:embed test.yml
var testYml string

const (
testVersion = "2.1.0"
)

func TestMain(m *testing.M) {
if !control.Enabled("AKO_CONTRACT_TEST") {
log.Printf("Skipping contract test as AKO_CONTRACT_TEST is unset")
return
}
l := launcher.NewFromEnv(testVersion)
if err := l.Launch(
testYml,
launcher.WaitReady("atlasprojects/my-project", time.Minute)); err != nil {
log.Fatalf("Failed to launch test bed: %v", err)
}
if !control.Enabled("SKIP_CLEANUP") { // allow to reuse Atlas resources for local tests
defer l.Cleanup()
}
os.Exit(m.Run())
}

func TestDefaultAuditingGet(t *testing.T) {
testProjectID := mustReadProjectID()
ctx := context.Background()
as := audit.NewAuditLog(contract.MustVersionedClient(t, ctx).AuditingApi)

result, err := as.Get(ctx, testProjectID)

require.NoError(t, err)
result.ConfigurationType = "" // Do not expect the returned cfg type to match
if result.AuditFilter == "{}" {
// Support re-runs, as we cannot get the filter back to empty
result.AuditFilter = ""
}
assert.Equal(t, defaultAtlasAuditing(), result)
}

func defaultAtlasAuditing() *audit.AuditConfig {
return &audit.AuditConfig{
Enabled: false,
AuditAuthorizationSuccess: false,
AuditFilter: "",
}
}

func TestSyncs(t *testing.T) {
testCases := []struct {
title string
auditing *audit.AuditConfig
}{
{
title: "Just enabled",
auditing: &audit.AuditConfig{
Enabled: true,
AuditAuthorizationSuccess: false,
AuditFilter: "{}", // must sent empty JSON to overwrite previous state
},
},
{
title: "Auth success logs as well",
auditing: &audit.AuditConfig{
Enabled: true,
AuditAuthorizationSuccess: true,
AuditFilter: "{}",
},
},
{
title: "With a filter",
auditing: &audit.AuditConfig{
Enabled: true,
AuditAuthorizationSuccess: false,
AuditFilter: `{"atype":"authenticate"}`,
},
},
{
title: "With a filter and success logs",
auditing: &audit.AuditConfig{
Enabled: true,
AuditAuthorizationSuccess: true,
AuditFilter: `{"atype":"authenticate"}`,
},
},
{
title: "All set but disabled",
auditing: &audit.AuditConfig{
Enabled: false,
AuditAuthorizationSuccess: true,
AuditFilter: `{"atype":"authenticate"}`,
},
},
{
title: "Default (disabled) case",
auditing: &audit.AuditConfig{
Enabled: false,
AuditAuthorizationSuccess: false,
AuditFilter: "{}",
},
},
}
testProjectID := mustReadProjectID()
ctx := context.Background()
as := audit.NewAuditLog(contract.MustVersionedClient(t, ctx).AuditingApi)

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
err := as.Set(ctx, testProjectID, tc.auditing)
require.NoError(t, err)

result, err := as.Get(ctx, testProjectID)
require.NoError(t, err)
result.ConfigurationType = "" // Do not expect the returned cfg type to match
assert.Equal(t, tc.auditing, result)
})
}
}

func mustReadProjectID() string {
l := launcher.NewFromEnv(testVersion)
output, err := l.Kubectl("get", "atlasprojects/my-project", "-o=jsonpath={.status.id}")
if err != nil {
log.Fatalf("Failed to get test project id: %v", err)
}
return output
}
6 changes: 6 additions & 0 deletions test/contract/audit/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
apiVersion: atlas.mongodb.com/v1
kind: AtlasProject
metadata:
name: my-project
spec:
name: Test Atlas Operator Project
26 changes: 26 additions & 0 deletions test/contract/contract.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package contract

import (
"context"
"os"
"testing"

"go.mongodb.org/atlas-sdk/v20231115008/admin"

"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
)

func NewVersionedClient(ctx context.Context) (*admin.APIClient, error) {
domain := os.Getenv("MCLI_OPS_MANAGER_URL")
pubKey := os.Getenv("MCLI_PUBLIC_API_KEY")
prvKey := os.Getenv("MCLI_PRIVATE_API_KEY")
return atlas.NewClient(domain, pubKey, prvKey)
}

func MustVersionedClient(t *testing.T, ctx context.Context) *admin.APIClient {
client, err := NewVersionedClient(ctx)
if err != nil {
t.Fatalf("Failed to get Atlas versioned client: %v", err)
}
return client
}
4 changes: 1 addition & 3 deletions test/e2e/e2e_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,7 @@ var (
)

func TestE2e(t *testing.T) {
if !control.Enabled("AKO_E2E_TEST") {
t.Skip("Skipping e2e tests, AKO_E2E_TEST is not set")
}
control.SkipTestUnless(t, "AKO_E2E_TEST")

RegisterFailHandler(Fail)
RunSpecs(t, "Atlas Operator E2E Test Suite")
Expand Down
Loading
Loading