Skip to content
Open
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
43 changes: 43 additions & 0 deletions api/v1alpha1/multigrescluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,49 @@ type MultiadminConfig struct {
// TemplateRef refers to a CoreTemplate to load configuration from.
// +optional
TemplateRef TemplateRef `json:"templateRef,omitempty"`

// Auth configures authentication for Multiadmin's gRPC, HTTP, Connect,
// and REST surfaces. Unset means no authentication is enforced.
// +optional
Auth *MultiadminAuthConfig `json:"auth,omitempty"`
}

// JWTAuth returns the JWT auth config for Multiadmin, or nil if unset. Safe
// to call on a nil receiver, since Spec.Multiadmin itself is optional.
func (c *MultiadminConfig) JWTAuth() *MultiadminJWTAuthConfig {
if c == nil || c.Auth == nil {
return nil
}
return c.Auth.JWT
}

// MultiadminAuthConfig configures authentication for Multiadmin.
type MultiadminAuthConfig struct {
// JWT configures JWT bearer-token authentication. When set, Multiadmin
// is started with --enable-auth and the corresponding --grpc-auth-jwt-*
// flags, requiring a valid JWT on its HTTP/Connect/REST/pprof surface.
// gRPC is unaffected and stays unauthenticated.
// +optional
JWT *MultiadminJWTAuthConfig `json:"jwt,omitempty"`
}

// MultiadminJWTAuthConfig configures --enable-auth (JWT bearer-token
// authentication for Multiadmin's HTTP/Connect/REST/pprof surface).
type MultiadminJWTAuthConfig struct {
// Issuer is the expected `iss` claim of presented JWTs.
// +kubebuilder:validation:MinLength=1
Issuer string `json:"issuer"`

// JWKSURI is the URI of the issuer's JWKS endpoint, used to verify JWT
// signatures.
// +kubebuilder:validation:MinLength=1
JWKSURI string `json:"jwksURI"`

// AllowedSubjects restricts which `sub` claims are authorized. If empty,
// any subject presenting a valid token from the trusted issuer is
// authorized.
// +optional
AllowedSubjects []string `json:"allowedSubjects,omitempty"`
}

// MultiadminWebConfig defines the configuration for MultiadminWeb in the Cluster.
Expand Down
45 changes: 45 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions config/crd/bases/multigres.com_multigresclusters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8626,6 +8626,42 @@ spec:
description: Multiadmin defines the configuration for the Multiadmin
component.
properties:
auth:
description: |-
Auth configures authentication for Multiadmin's gRPC, HTTP, Connect,
and REST surfaces. Unset means no authentication is enforced.
properties:
jwt:
description: |-
JWT configures JWT bearer-token authentication. When set, Multiadmin
is started with --enable-auth and the corresponding --grpc-auth-jwt-*
flags, requiring a valid JWT on its HTTP/Connect/REST/pprof surface.
gRPC is unaffected and stays unauthenticated.
properties:
allowedSubjects:
description: |-
AllowedSubjects restricts which `sub` claims are authorized. If empty,
any subject presenting a valid token from the trusted issuer is
authorized.
items:
type: string
type: array
issuer:
description: Issuer is the expected `iss` claim of presented
JWTs.
minLength: 1
type: string
jwksURI:
description: |-
JWKSURI is the URI of the issuer's JWKS endpoint, used to verify JWT
signatures.
minLength: 1
type: string
required:
- issuer
- jwksURI
type: object
type: object
spec:
description: Spec defines the inline configuration.
properties:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,19 @@ func BuildMultiadminDeployment(
)
}

if jwtAuth := cluster.Spec.Multiadmin.JWTAuth(); jwtAuth != nil {
podSpec := &deploy.Spec.Template.Spec
podSpec.Containers[0].Args = append(podSpec.Containers[0].Args,
"--enable-auth",
"--grpc-auth-jwt-issuer="+jwtAuth.Issuer,
"--grpc-auth-jwt-jwks-uri="+jwtAuth.JWKSURI,
)
for _, sub := range jwtAuth.AllowedSubjects {
podSpec.Containers[0].Args = append(podSpec.Containers[0].Args,
"--grpc-auth-jwt-allowed-subs="+sub)
}
}

if err := controllerutil.SetControllerReference(cluster, deploy, scheme); err != nil {
return nil, err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,89 @@ func TestBuildMultiadminDeployment(t *testing.T) {
})
}

t.Run("Success with JWT auth", func(t *testing.T) {
jwtCluster := cluster.DeepCopy()
jwtCluster.Spec.Multiadmin = &multigresv1alpha1.MultiadminConfig{
Auth: &multigresv1alpha1.MultiadminAuthConfig{
JWT: &multigresv1alpha1.MultiadminJWTAuthConfig{
Issuer: "https://issuer.example.com",
JWKSURI: "https://issuer.example.com/.well-known/jwks.json",
AllowedSubjects: []string{
"arn:aws:iam::123456789012:role/worker",
"arn:aws:iam::123456789012:role/mgmt-api",
},
},
},
}

got, err := BuildMultiadminDeployment(jwtCluster, spec, scheme)
if err != nil {
t.Fatalf("BuildMultiadminDeployment() error = %v", err)
}

wantArgs := []string{
"--enable-auth",
"--grpc-auth-jwt-issuer=https://issuer.example.com",
"--grpc-auth-jwt-jwks-uri=https://issuer.example.com/.well-known/jwks.json",
"--grpc-auth-jwt-allowed-subs=arn:aws:iam::123456789012:role/worker",
"--grpc-auth-jwt-allowed-subs=arn:aws:iam::123456789012:role/mgmt-api",
}
container := got.Spec.Template.Spec.Containers[0]
tailArgs := container.Args[len(container.Args)-len(wantArgs):]
if diff := cmp.Diff(wantArgs, tailArgs); diff != "" {
t.Errorf("JWT auth args mismatch (-want +got):\n%s", diff)
}
})

t.Run("Success with JWT auth and no allowed subjects", func(t *testing.T) {
jwtCluster := cluster.DeepCopy()
jwtCluster.Spec.Multiadmin = &multigresv1alpha1.MultiadminConfig{
Auth: &multigresv1alpha1.MultiadminAuthConfig{
JWT: &multigresv1alpha1.MultiadminJWTAuthConfig{
Issuer: "https://issuer.example.com",
JWKSURI: "https://issuer.example.com/.well-known/jwks.json",
},
},
}

got, err := BuildMultiadminDeployment(jwtCluster, spec, scheme)
if err != nil {
t.Fatalf("BuildMultiadminDeployment() error = %v", err)
}

wantArgs := []string{
"--enable-auth",
"--grpc-auth-jwt-issuer=https://issuer.example.com",
"--grpc-auth-jwt-jwks-uri=https://issuer.example.com/.well-known/jwks.json",
}
container := got.Spec.Template.Spec.Containers[0]
tailArgs := container.Args[len(container.Args)-len(wantArgs):]
if diff := cmp.Diff(wantArgs, tailArgs); diff != "" {
t.Errorf("JWT auth args mismatch (-want +got):\n%s", diff)
}
})

for name, mutateCluster := range map[string]func(*multigresv1alpha1.MultigresCluster){
"nil Multiadmin config": func(*multigresv1alpha1.MultigresCluster) {},
"Multiadmin config with no auth": func(cluster *multigresv1alpha1.MultigresCluster) {
cluster.Spec.Multiadmin = &multigresv1alpha1.MultiadminConfig{}
},
} {
t.Run("No JWT auth args when "+name, func(t *testing.T) {
noAuthCluster := cluster.DeepCopy()
mutateCluster(noAuthCluster)
got, err := BuildMultiadminDeployment(noAuthCluster, spec, scheme)
if err != nil {
t.Fatalf("BuildMultiadminDeployment() error = %v", err)
}
for _, arg := range got.Spec.Template.Spec.Containers[0].Args {
if arg == "--enable-auth" {
t.Errorf("unexpected JWT auth argument %q", arg)
}
}
})
}

t.Run("ControllerRefError", func(t *testing.T) {
emptyScheme := runtime.NewScheme()
_, err := BuildMultiadminDeployment(cluster, spec, emptyScheme)
Expand Down
Loading