Skip to content
Draft
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
40 changes: 40 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,46 @@ function e2e() {
make e2e_test
}

## e2e_gotest - Runs only the e2e Go test suite against the already deployed cluster.
function e2e_gotest() {
make e2e_test_run_gotest
}

## e2e_gotest_single - Runs a single e2e test by regex pattern.
function e2e_gotest_single() {
USE_GKE_E2E_AUTH_PLUGIN=True \
TEST_INFRA_JSON="$PWD/bin/testinfra.json" \
go test --count=1 -v -race ./tests/... -run "$1"
}

## e2e_logs - Fetches logs from the operator controller manager in the e2e cluster.
function e2e_logs() {
USE_GKE_E2E_AUTH_PLUGIN=True bin/kubectl --kubeconfig=bin/e2e-kubeconfig.yaml logs -n cloud-sql-proxy-operator-system deployment/cloud-sql-proxy-operator-controller-manager --tail=200 "$@"
}

## e2e_status - Checks pod and service status in the e2e clusters.
function e2e_status() {
echo "=== Public E2E Cluster ==="
USE_GKE_E2E_AUTH_PLUGIN=True bin/kubectl --kubeconfig=bin/e2e-kubeconfig.yaml get pods,svc,crd -A
echo "=== Private E2E Cluster ==="
USE_GKE_E2E_AUTH_PLUGIN=True bin/kubectl --kubeconfig=bin/e2e-private-kubeconfig.yaml get pods,svc,crd -A
}

## kubectl_cmd - Runs kubectl on the public e2e cluster.
function kubectl_cmd() {
USE_GKE_E2E_AUTH_PLUGIN=True bin/kubectl --kubeconfig=bin/e2e-kubeconfig.yaml "$@"
}

## kubectl_private_cmd - Runs kubectl on the private e2e cluster.
function kubectl_private_cmd() {
USE_GKE_E2E_AUTH_PLUGIN=True bin/kubectl --kubeconfig=bin/e2e-private-kubeconfig.yaml "$@"
}

## e2e_deploy - Deploys CRD and operator to both e2e clusters.
function e2e_deploy() {
make e2e_build_deploy
}

function get_golang_tool() {
name="$1"
github_repo="$2"
Expand Down
243 changes: 243 additions & 0 deletions internal/api/v1/authproxyworkload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,18 @@ package v1_test

import (
"context"
"fmt"
"testing"

cloudsqlapi "github.com/GoogleCloudPlatform/cloud-sql-proxy-operator/internal/api/v1"
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
authorizationv1 "k8s.io/api/authorization/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

func ptr[T int | int32 | int64 | string | bool](i T) *T {
Expand Down Expand Up @@ -596,3 +603,239 @@ func printFieldErrors(t *testing.T, err error) {
}
}
}

type fakeSARClient struct {
client.Client
allowedRules func(spec authorizationv1.SubjectAccessReviewSpec) bool
}

func (f *fakeSARClient) Create(_ context.Context, obj client.Object, _ ...client.CreateOption) error {
sar, ok := obj.(*authorizationv1.SubjectAccessReview)
if !ok {
return fmt.Errorf("unexpected object type %T", obj)
}
if f.allowedRules != nil {
sar.Status.Allowed = f.allowedRules(sar.Spec)
} else {
sar.Status.Allowed = true
}
return nil
}

func TestAuthProxyWorkload_ValidateAuthorization(t *testing.T) {
tests := []struct {
desc string
spec cloudsqlapi.AuthProxyWorkloadSpec
allowedRules func(spec authorizationv1.SubjectAccessReviewSpec) bool
wantAllowed bool
}{
{
desc: "Allowed: User has pod delete and deployment update/patch",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "Deployment",
Name: "my-deployment",
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(_ authorizationv1.SubjectAccessReviewSpec) bool {
return true
},
wantAllowed: true,
},
{
desc: "Denied: User lacks pod delete permission",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "Deployment",
Name: "my-deployment",
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(spec authorizationv1.SubjectAccessReviewSpec) bool {
if spec.ResourceAttributes != nil && spec.ResourceAttributes.Resource == "pods" && spec.ResourceAttributes.Verb == "delete" {
return false
}
return true
},
wantAllowed: false,
},
{
desc: "Denied: User lacks deployment update permission",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "Deployment",
Name: "my-deployment",
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(spec authorizationv1.SubjectAccessReviewSpec) bool {
if spec.ResourceAttributes != nil && spec.ResourceAttributes.Resource == "deployments" {
return false
}
return true
},
wantAllowed: false,
},
{
desc: "Allowed: User has wildcard permission for label selector",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "StatefulSet",
Selector: &v1.LabelSelector{
MatchLabels: map[string]string{"app": "db"},
},
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(_ authorizationv1.SubjectAccessReviewSpec) bool {
return true
},
wantAllowed: true,
},
{
desc: "Denied: User lacks wildcard permission for label selector",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "StatefulSet",
Selector: &v1.LabelSelector{
MatchLabels: map[string]string{"app": "db"},
},
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(spec authorizationv1.SubjectAccessReviewSpec) bool {
if spec.ResourceAttributes != nil && spec.ResourceAttributes.Resource == "statefulsets" {
return false
}
return true
},
wantAllowed: false,
},
{
desc: "Allowed: User has container override permission when specifying custom container",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "Deployment",
Name: "my-deployment",
},
AuthProxyContainer: &cloudsqlapi.AuthProxyContainerSpec{
Container: &corev1.Container{
Name: "custom-proxy",
Image: "custom-image:v1",
},
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(_ authorizationv1.SubjectAccessReviewSpec) bool {
return true
},
wantAllowed: true,
},
{
desc: "Denied: User lacks container override permission when specifying custom container",
spec: cloudsqlapi.AuthProxyWorkloadSpec{
Workload: cloudsqlapi.WorkloadSelectorSpec{
Kind: "Deployment",
Name: "my-deployment",
},
AuthProxyContainer: &cloudsqlapi.AuthProxyContainerSpec{
Container: &corev1.Container{
Name: "custom-proxy",
Image: "custom-image:v1",
},
},
Instances: []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db1",
PortEnvName: "DB_PORT",
}},
},
allowedRules: func(spec authorizationv1.SubjectAccessReviewSpec) bool {
if spec.ResourceAttributes != nil && spec.ResourceAttributes.Subresource == "containeroverride" {
return false
}
return true
},
wantAllowed: false,
},
}

for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
p := &cloudsqlapi.AuthProxyWorkload{
ObjectMeta: v1.ObjectMeta{
Name: "test-authproxy",
Namespace: "test-ns",
},
Spec: tc.spec,
}

req := admissionRequest("alice")
ctx := admission.NewContextWithRequest(context.Background(), req)

validator := &cloudsqlapi.AuthProxyWorkloadValidator{
Client: &fakeSARClient{
allowedRules: tc.allowedRules,
},
}

_, err := validator.ValidateCreate(ctx, p)
if tc.wantAllowed && err != nil {
t.Fatalf("expected allowed, got error: %v", err)
}
if !tc.wantAllowed && err == nil {
t.Fatalf("expected forbidden error, got nil")
}

// Also verify ValidateUpdate with modified spec
oldObj := p.DeepCopy()
oldObj.Spec.Instances = []cloudsqlapi.InstanceSpec{{
ConnectionString: "proj:region:db2",
PortEnvName: "DB_PORT",
}}
_, err = validator.ValidateUpdate(ctx, oldObj, p)
if tc.wantAllowed && err != nil {
t.Fatalf("expected update allowed, got error: %v", err)
}
if !tc.wantAllowed && err == nil {
t.Fatalf("expected update forbidden error, got nil")
}

// Verify ValidateUpdate with unchanged spec (e.g. controller adding finalizers) is always allowed
_, err = validator.ValidateUpdate(ctx, p, p)
if err != nil {
t.Fatalf("expected update with unchanged spec to be allowed, got: %v", err)
}
})
}
}

func admissionRequest(user string) admission.Request {
return admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
UserInfo: authenticationv1.UserInfo{
Username: user,
UID: "uid-" + user,
Groups: []string{"system:authenticated"},
},
},
}
}
Loading
Loading