Skip to content

Commit b8a2adf

Browse files
authored
[skunkworks] Auditing contract testing (#1589)
* [skunkworks] Auditing contract testing Signed-off-by: jose.vazquez <[email protected]> * Rename & refactor service * Renames for consistency Signed-off-by: jose.vazquez <[email protected]> * Applied Sergiusz suggestions Signed-off-by: jose.vazquez <[email protected]> --------- Signed-off-by: jose.vazquez <[email protected]>
1 parent 8846391 commit b8a2adf

File tree

19 files changed

+719
-11
lines changed

19 files changed

+719
-11
lines changed

.github/workflows/cloud-tests.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ jobs:
5454
with:
5555
forked: ${{ inputs.forked }}
5656

57+
contract-tests:
58+
needs: allowed
59+
uses: ./.github/workflows/test-contract.yml
60+
secrets: inherit
61+
5762
e2e-tests:
5863
needs: allowed
5964
uses: ./.github/workflows/test-e2e.yml
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
name: Contract Tests
2+
3+
on:
4+
workflow_call:
5+
workflow_dispatch:
6+
7+
jobs:
8+
contract:
9+
name: Contract Tests
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Check out code
13+
uses: actions/checkout@v4
14+
with:
15+
ref: ${{github.event.pull_request.head.sha}}
16+
17+
- name: Setup Go
18+
uses: actions/setup-go@v5
19+
with:
20+
go-version-file: "${{ github.workspace }}/go.mod"
21+
cache: false
22+
23+
- name: Create k8s Kind Cluster
24+
uses: helm/[email protected]
25+
with:
26+
version: v0.22.0
27+
config: test/helper/e2e/config/kind.yaml
28+
node_image: kindest/node:v1.29.2
29+
30+
- name: Run Contract Testing
31+
env:
32+
AKO_CONTRACT_TEST: 1
33+
MCLI_OPS_MANAGER_URL: https://cloud-qa.mongodb.com
34+
MCLI_ORG_ID: ${{ secrets.ATLAS_ORG_ID }}
35+
MCLI_PUBLIC_API_KEY: ${{ secrets.ATLAS_PUBLIC_KEY }}
36+
MCLI_PRIVATE_API_KEY: ${{ secrets.ATLAS_PRIVATE_KEY }}
37+
run: make contract-tests

Makefile

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -562,3 +562,8 @@ upload-sbom-to-silk: ## Upload a give SBOM (lite) to Silk
562562
@ARTIFACTORY_USERNAME=$(ARTIFACTORY_USERNAME) ARTIFACTORY_PASSWORD=$(ARTIFACTORY_PASSWORD) \
563563
SILK_CLIENT_ID=$(SILK_CLIENT_ID) SILK_CLIENT_SECRET=$(SILK_CLIENT_SECRET) \
564564
SILK_ASSET_GROUP=$(SILK_ASSET_GROUP) ./scripts/upload-to-silk.sh $(SBOM_JSON_FILE)
565+
566+
.PHONY: contract-tests
567+
contract-tests: ## Run contract tests
568+
go clean -testcache
569+
AKO_CONTRACT_TEST=1 go test -v -race -cover ./test/contract/...
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package audit
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"go.mongodb.org/atlas-sdk/v20231115008/admin"
8+
"go.uber.org/zap"
9+
"k8s.io/apimachinery/pkg/types"
10+
11+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation"
12+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
13+
)
14+
15+
// AuditLogService is the interface exposed by this translation layer over
16+
// the Atlas AuditLog
17+
type AuditLogService interface {
18+
Get(ctx context.Context, projectID string) (*AuditConfig, error)
19+
Set(ctx context.Context, projectID string, auditing *AuditConfig) error
20+
}
21+
22+
// AuditLog is the default implementation of the AuditLogService using the Atlas SDK
23+
type AuditLog struct {
24+
auditAPI admin.AuditingApi
25+
}
26+
27+
// NewAuditLogService creates an AuditLog from credentials and the atlas provider
28+
func NewAuditLogService(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*AuditLog, error) {
29+
client, err := translation.NewVersionedClient(ctx, provider, secretRef, log)
30+
if err != nil {
31+
return nil, err
32+
}
33+
return NewAuditLog(client.AuditingApi), nil
34+
}
35+
36+
// NewAuditLog wraps the SDK AuditingApi as an AuditLog
37+
func NewAuditLog(api admin.AuditingApi) *AuditLog {
38+
return &AuditLog{auditAPI: api}
39+
}
40+
41+
// Get an Atlas Project audit log configuration
42+
func (s *AuditLog) Get(ctx context.Context, projectID string) (*AuditConfig, error) {
43+
auditLog, _, err := s.auditAPI.GetAuditingConfiguration(ctx, projectID).Execute()
44+
if err != nil {
45+
return nil, fmt.Errorf("failed to get audit log from Atlas: %w", err)
46+
}
47+
return fromAtlas(auditLog)
48+
}
49+
50+
// Set an Atlas Project audit log configuration
51+
func (s *AuditLog) Set(ctx context.Context, projectID string, auditing *AuditConfig) error {
52+
_, _, err := s.auditAPI.UpdateAuditingConfiguration(ctx, projectID, toAtlas(auditing)).Execute()
53+
return err
54+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package audit
2+
3+
import (
4+
"fmt"
5+
6+
"go.mongodb.org/atlas-sdk/v20231115008/admin"
7+
8+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer"
9+
)
10+
11+
type AuditingConfigType string
12+
13+
const (
14+
None AuditingConfigType = "NONE"
15+
FilterBuilder AuditingConfigType = "FILTER_BUILDER"
16+
FilterJSON AuditingConfigType = "FILTER_JSON"
17+
)
18+
19+
// AuditConfig represents the Atlas Project audit log config
20+
type AuditConfig struct {
21+
Enabled bool
22+
AuditAuthorizationSuccess bool
23+
ConfigurationType AuditingConfigType
24+
AuditFilter string
25+
}
26+
27+
func toAtlas(auditing *AuditConfig) *admin.AuditLog {
28+
return &admin.AuditLog{
29+
Enabled: pointer.MakePtr(auditing.Enabled),
30+
AuditAuthorizationSuccess: pointer.MakePtr(auditing.AuditAuthorizationSuccess),
31+
AuditFilter: pointer.MakePtr(auditing.AuditFilter),
32+
// ConfigurationType is not set on the PATCH operation to Atlas
33+
}
34+
}
35+
36+
func fromAtlas(auditLog *admin.AuditLog) (*AuditConfig, error) {
37+
cfgType, err := configTypeFromAtlas(auditLog.ConfigurationType)
38+
if err != nil {
39+
return nil, err
40+
}
41+
return &AuditConfig{
42+
Enabled: pointer.GetOrDefault(auditLog.Enabled, false),
43+
AuditAuthorizationSuccess: pointer.GetOrDefault(auditLog.AuditAuthorizationSuccess, false),
44+
ConfigurationType: cfgType,
45+
AuditFilter: pointer.GetOrDefault(auditLog.AuditFilter, ""),
46+
}, nil
47+
}
48+
49+
func configTypeFromAtlas(configType *string) (AuditingConfigType, error) {
50+
ct := pointer.GetOrDefault(configType, string(None))
51+
switch ct {
52+
case string(None), string(FilterBuilder), string(FilterJSON):
53+
return AuditingConfigType(ct), nil
54+
default:
55+
return AuditingConfigType(ct), fmt.Errorf("unsupported Auditing Config type %q", ct)
56+
}
57+
}

internal/translation/client.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package translation
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"go.mongodb.org/atlas-sdk/v20231115008/admin"
8+
"go.uber.org/zap"
9+
"k8s.io/apimachinery/pkg/types"
10+
11+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
12+
)
13+
14+
func NewVersionedClient(ctx context.Context, provider atlas.Provider, secretRef *types.NamespacedName, log *zap.SugaredLogger) (*admin.APIClient, error) {
15+
apiClient, _, err := provider.SdkClient(ctx, secretRef, log)
16+
if err != nil {
17+
return nil, fmt.Errorf("failed to instantiate Versioned Atlas client: %w", err)
18+
}
19+
return apiClient, nil
20+
}

test/contract/audit/audit_test.go

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
package audit
2+
3+
import (
4+
"context"
5+
_ "embed"
6+
"log"
7+
"os"
8+
"testing"
9+
"time"
10+
11+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/audit"
12+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/control"
13+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/test/helper/launcher"
14+
15+
"github.com/stretchr/testify/assert"
16+
"github.com/stretchr/testify/require"
17+
18+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/test/contract"
19+
)
20+
21+
//go:embed test.yml
22+
var testYml string
23+
24+
const (
25+
testVersion = "2.1.0"
26+
)
27+
28+
func TestMain(m *testing.M) {
29+
if !control.Enabled("AKO_CONTRACT_TEST") {
30+
log.Printf("Skipping contract test as AKO_CONTRACT_TEST is unset")
31+
return
32+
}
33+
l := launcher.NewFromEnv(testVersion)
34+
if err := l.Launch(
35+
testYml,
36+
launcher.WaitReady("atlasprojects/my-project", time.Minute)); err != nil {
37+
log.Fatalf("Failed to launch test bed: %v", err)
38+
}
39+
if !control.Enabled("SKIP_CLEANUP") { // allow to reuse Atlas resources for local tests
40+
defer l.Cleanup()
41+
}
42+
os.Exit(m.Run())
43+
}
44+
45+
func TestDefaultAuditingGet(t *testing.T) {
46+
testProjectID := mustReadProjectID()
47+
ctx := context.Background()
48+
as := audit.NewAuditLog(contract.MustVersionedClient(t, ctx).AuditingApi)
49+
50+
result, err := as.Get(ctx, testProjectID)
51+
52+
require.NoError(t, err)
53+
result.ConfigurationType = "" // Do not expect the returned cfg type to match
54+
if result.AuditFilter == "{}" {
55+
// Support re-runs, as we cannot get the filter back to empty
56+
result.AuditFilter = ""
57+
}
58+
assert.Equal(t, defaultAtlasAuditing(), result)
59+
}
60+
61+
func defaultAtlasAuditing() *audit.AuditConfig {
62+
return &audit.AuditConfig{
63+
Enabled: false,
64+
AuditAuthorizationSuccess: false,
65+
AuditFilter: "",
66+
}
67+
}
68+
69+
func TestSyncs(t *testing.T) {
70+
testCases := []struct {
71+
title string
72+
auditing *audit.AuditConfig
73+
}{
74+
{
75+
title: "Just enabled",
76+
auditing: &audit.AuditConfig{
77+
Enabled: true,
78+
AuditAuthorizationSuccess: false,
79+
AuditFilter: "{}", // must sent empty JSON to overwrite previous state
80+
},
81+
},
82+
{
83+
title: "Auth success logs as well",
84+
auditing: &audit.AuditConfig{
85+
Enabled: true,
86+
AuditAuthorizationSuccess: true,
87+
AuditFilter: "{}",
88+
},
89+
},
90+
{
91+
title: "With a filter",
92+
auditing: &audit.AuditConfig{
93+
Enabled: true,
94+
AuditAuthorizationSuccess: false,
95+
AuditFilter: `{"atype":"authenticate"}`,
96+
},
97+
},
98+
{
99+
title: "With a filter and success logs",
100+
auditing: &audit.AuditConfig{
101+
Enabled: true,
102+
AuditAuthorizationSuccess: true,
103+
AuditFilter: `{"atype":"authenticate"}`,
104+
},
105+
},
106+
{
107+
title: "All set but disabled",
108+
auditing: &audit.AuditConfig{
109+
Enabled: false,
110+
AuditAuthorizationSuccess: true,
111+
AuditFilter: `{"atype":"authenticate"}`,
112+
},
113+
},
114+
{
115+
title: "Default (disabled) case",
116+
auditing: &audit.AuditConfig{
117+
Enabled: false,
118+
AuditAuthorizationSuccess: false,
119+
AuditFilter: "{}",
120+
},
121+
},
122+
}
123+
testProjectID := mustReadProjectID()
124+
ctx := context.Background()
125+
as := audit.NewAuditLog(contract.MustVersionedClient(t, ctx).AuditingApi)
126+
127+
for _, tc := range testCases {
128+
t.Run(tc.title, func(t *testing.T) {
129+
err := as.Set(ctx, testProjectID, tc.auditing)
130+
require.NoError(t, err)
131+
132+
result, err := as.Get(ctx, testProjectID)
133+
require.NoError(t, err)
134+
result.ConfigurationType = "" // Do not expect the returned cfg type to match
135+
assert.Equal(t, tc.auditing, result)
136+
})
137+
}
138+
}
139+
140+
func mustReadProjectID() string {
141+
l := launcher.NewFromEnv(testVersion)
142+
output, err := l.Kubectl("get", "atlasprojects/my-project", "-o=jsonpath={.status.id}")
143+
if err != nil {
144+
log.Fatalf("Failed to get test project id: %v", err)
145+
}
146+
return output
147+
}

test/contract/audit/test.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
apiVersion: atlas.mongodb.com/v1
2+
kind: AtlasProject
3+
metadata:
4+
name: my-project
5+
spec:
6+
name: Test Atlas Operator Project

test/contract/contract.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package contract
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"go.mongodb.org/atlas-sdk/v20231115008/admin"
9+
10+
"github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas"
11+
)
12+
13+
func NewVersionedClient(ctx context.Context) (*admin.APIClient, error) {
14+
domain := os.Getenv("MCLI_OPS_MANAGER_URL")
15+
pubKey := os.Getenv("MCLI_PUBLIC_API_KEY")
16+
prvKey := os.Getenv("MCLI_PRIVATE_API_KEY")
17+
return atlas.NewClient(domain, pubKey, prvKey)
18+
}
19+
20+
func MustVersionedClient(t *testing.T, ctx context.Context) *admin.APIClient {
21+
client, err := NewVersionedClient(ctx)
22+
if err != nil {
23+
t.Fatalf("Failed to get Atlas versioned client: %v", err)
24+
}
25+
return client
26+
}

test/e2e/e2e_suite_test.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ var (
2626
)
2727

2828
func TestE2e(t *testing.T) {
29-
if !control.Enabled("AKO_E2E_TEST") {
30-
t.Skip("Skipping e2e tests, AKO_E2E_TEST is not set")
31-
}
29+
control.SkipTestUnless(t, "AKO_E2E_TEST")
3230

3331
RegisterFailHandler(Fail)
3432
RunSpecs(t, "Atlas Operator E2E Test Suite")

0 commit comments

Comments
 (0)