Skip to content

Commit 821e1f5

Browse files
Merge pull request #514 from jamesglennan/506_usernamCELValidation
Add CertificateRequest username to CEL Validator with serviceaccount functions
2 parents da180af + 5ccd683 commit 821e1f5

File tree

6 files changed

+231
-8
lines changed

6 files changed

+231
-8
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ require (
1717
k8s.io/api v0.31.1
1818
k8s.io/apiextensions-apiserver v0.31.1
1919
k8s.io/apimachinery v0.31.1
20+
k8s.io/apiserver v0.31.1
2021
k8s.io/cli-runtime v0.31.1
2122
k8s.io/client-go v0.31.1
2223
k8s.io/component-base v0.31.1
@@ -111,7 +112,6 @@ require (
111112
gopkg.in/inf.v0 v0.9.1 // indirect
112113
gopkg.in/yaml.v2 v2.4.0 // indirect
113114
gopkg.in/yaml.v3 v3.0.1 // indirect
114-
k8s.io/apiserver v0.31.1 // indirect
115115
k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect
116116
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect
117117
sigs.k8s.io/gateway-api v1.1.0 // indirect

pkg/internal/approver/validation/certificaterequest.pb.go

Lines changed: 17 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/internal/approver/validation/certificaterequest.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ option go_package = "github.com/cert-manager/approver-policy/pkg/internal/approv
77
message CertificateRequest {
88
string name = 1;
99
string namespace = 2;
10+
string username = 3;
1011
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/*
2+
Copyright 2024 The cert-manager Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package validation
18+
19+
import (
20+
"fmt"
21+
"reflect"
22+
23+
"github.com/google/cel-go/cel"
24+
"github.com/google/cel-go/common/types"
25+
"github.com/google/cel-go/common/types/ref"
26+
"k8s.io/apiserver/pkg/authentication/serviceaccount"
27+
)
28+
29+
var (
30+
SAType = cel.ObjectType("cm.io.policy.pkg.internal.approver.validation.ServiceAccount")
31+
)
32+
33+
type saLib struct{}
34+
type ServiceAccount struct {
35+
Name string
36+
Namespace string
37+
}
38+
39+
func ServiceAccountLib() cel.EnvOption {
40+
return cel.Lib(&saLib{})
41+
}
42+
43+
// ConvertToNative implements ref.Val.ConvertToNative.
44+
func (sa ServiceAccount) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
45+
if reflect.TypeOf(sa).AssignableTo(typeDesc) {
46+
return sa, nil
47+
}
48+
if reflect.TypeOf("").AssignableTo(typeDesc) {
49+
return serviceaccount.MakeUsername(sa.Namespace, sa.Name), nil
50+
}
51+
return nil, fmt.Errorf("type conversion error from 'serviceaccount' to '%v'", typeDesc)
52+
}
53+
54+
// ConvertToType implements ref.Val.ConvertToType.
55+
func (sa ServiceAccount) ConvertToType(typeVal ref.Type) ref.Val {
56+
switch typeVal {
57+
case SAType:
58+
return sa
59+
case types.TypeType:
60+
return SAType
61+
}
62+
return types.NewErr("type conversion error from '%s' to '%s'", SAType, typeVal)
63+
}
64+
65+
// Equal implements ref.Val.Equal.
66+
func (sa ServiceAccount) Equal(other ref.Val) ref.Val {
67+
otherSA, ok := other.(ServiceAccount)
68+
if !ok {
69+
return types.MaybeNoSuchOverloadErr(other)
70+
}
71+
return types.Bool(sa.Name == otherSA.Name && sa.Namespace == otherSA.Namespace)
72+
}
73+
74+
// Type implements ref.Val.Type.Y
75+
func (sa ServiceAccount) Type() ref.Type {
76+
return SAType
77+
}
78+
79+
// Value implements ref.Val.Value.
80+
func (sa ServiceAccount) Value() interface{} {
81+
return sa
82+
}
83+
84+
var saLibraryDecls = map[string][]cel.FunctionOpt{
85+
"serviceAccount": {
86+
cel.Overload("username_to_serviceaccount", []*cel.Type{cel.StringType}, SAType,
87+
cel.UnaryBinding(stringToServiceAccount))},
88+
"getName": {
89+
cel.MemberOverload("serviceaccount_get_name", []*cel.Type{SAType}, cel.StringType,
90+
cel.UnaryBinding(getServiceAccountName))},
91+
"getNamespace": {
92+
cel.MemberOverload("serviceaccount_get_namespace", []*cel.Type{SAType}, cel.StringType,
93+
cel.UnaryBinding(getServiceAccountNamespace))},
94+
"isServiceAccount": {
95+
cel.Overload("serviceaccount_is_sa", []*cel.Type{cel.StringType}, cel.BoolType,
96+
cel.UnaryBinding(isServiceAccount))},
97+
}
98+
99+
func stringToServiceAccount(arg ref.Val) ref.Val {
100+
s, ok := arg.Value().(string)
101+
if !ok {
102+
return types.MaybeNoSuchOverloadErr(arg)
103+
}
104+
105+
ns, name, err := serviceaccount.SplitUsername(s)
106+
107+
if err != nil {
108+
return types.NewErr("Unable to convert to serviceaccount: err: %s, username: %s", err, s)
109+
}
110+
111+
return ServiceAccount{
112+
Name: name,
113+
Namespace: ns,
114+
}
115+
}
116+
117+
func isServiceAccount(arg ref.Val) ref.Val {
118+
s, ok := arg.Value().(string)
119+
if !ok {
120+
return types.MaybeNoSuchOverloadErr(arg)
121+
}
122+
123+
_, _, err := serviceaccount.SplitUsername(s)
124+
125+
if err != nil {
126+
return types.False
127+
}
128+
129+
return types.True
130+
}
131+
132+
func getServiceAccountName(arg ref.Val) ref.Val {
133+
s, ok := arg.Value().(ServiceAccount)
134+
if !ok {
135+
return types.MaybeNoSuchOverloadErr(arg)
136+
}
137+
138+
return types.String(s.Name)
139+
}
140+
141+
func getServiceAccountNamespace(arg ref.Val) ref.Val {
142+
s, ok := arg.Value().(ServiceAccount)
143+
if !ok {
144+
return types.MaybeNoSuchOverloadErr(arg)
145+
}
146+
147+
return types.String(s.Namespace)
148+
}
149+
150+
func (*saLib) CompileOptions() []cel.EnvOption {
151+
options := []cel.EnvOption{}
152+
for name, overloads := range saLibraryDecls {
153+
options = append(options, cel.Function(name, overloads...))
154+
}
155+
return options
156+
}
157+
158+
func (*saLib) ProgramOptions() []cel.ProgramOption {
159+
return []cel.ProgramOption{}
160+
}

pkg/internal/approver/validation/validator.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,9 @@ func (v *validator) compile() error {
6161
cel.Variable(varSelf, cel.StringType),
6262
cel.Variable(varRequest, cel.ObjectType("cm.io.policy.pkg.internal.approver.validation.CertificateRequest")),
6363
ext.Strings(),
64+
ServiceAccountLib(),
6465
)
66+
6567
if err != nil {
6668
return err
6769
}
@@ -89,6 +91,7 @@ func (v *validator) Validate(value string, request cmapi.CertificateRequest) (bo
8991
varRequest: &CertificateRequest{
9092
Name: request.GetName(),
9193
Namespace: request.GetNamespace(),
94+
Username: request.Spec.Username,
9295
},
9396
}
9497

pkg/internal/approver/validation/validator_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ func Test_Validator_Compile(t *testing.T) {
3838
{name: "err-undeclared-vars", expr: "foo = bar", wantErr: true},
3939
{name: "err-must-return-bool", expr: "size('foo')", wantErr: true},
4040
{name: "err-invalid-property", expr: "size(cr.foo) < 24", wantErr: true},
41+
{name: "check-username-property", expr: "size(cr.username) > 0", wantErr: false},
42+
{name: "check-serviceaccount-getname", expr: "self.startsWith(serviceAccount(cr.username).getName())", wantErr: false},
43+
{name: "check-serviceaccount-getnamespace", expr: "self.startsWith(serviceAccount(cr.username).getNamespace())", wantErr: false},
44+
{name: "check-serviceaccount-isSA", expr: "isServiceAccount(cr.username)", wantErr: false},
4145
}
4246
for _, tt := range tests {
4347
t.Run(tt.name, func(t *testing.T) {
@@ -91,3 +95,48 @@ func newCertificateRequest(namespace string) cmapi.CertificateRequest {
9195
request.SetNamespace(namespace)
9296
return request
9397
}
98+
99+
func Test_Validator_Validate_ServiceAccount(t *testing.T) {
100+
v := &validator{expression: "isServiceAccount(cr.username) && self.startsWith('spiffe://acme.com/ns/%s/sa/%s'.format([serviceAccount(cr.username).getNamespace(),serviceAccount(cr.username).getName()]))"}
101+
err := v.compile()
102+
assert.NoError(t, err)
103+
104+
type args struct {
105+
val string
106+
cr cmapi.CertificateRequest
107+
}
108+
tests := []struct {
109+
name string
110+
args args
111+
want bool
112+
wantErr bool
113+
}{
114+
{name: "correct-namespace-and-name", args: args{val: "spiffe://acme.com/ns/foo-ns/sa/bar", cr: newCertificateRequestWithUsername("system:serviceaccount:foo-ns:bar")}, want: true},
115+
{name: "correct-namespace-and-name2", args: args{val: "spiffe://acme.com/ns/bar-ns/sa/foo", cr: newCertificateRequestWithUsername("system:serviceaccount:bar-ns:foo")}, want: true},
116+
{name: "correct-namespace-wrong-name", args: args{val: "spiffe://acme.com/ns/foo-ns/sa/foo", cr: newCertificateRequestWithUsername("system:serviceaccount:foo-ns:bar")}, want: false},
117+
{name: "wrong-namespace-correct-name", args: args{val: "spiffe://acme.com/ns/foo-ns/sa/bar", cr: newCertificateRequestWithUsername("system:serviceaccount:bar-ns:bar")}, want: false},
118+
{name: "not-serviceaccount", args: args{val: "spiffe://acme.com/ns/foo-ns/sa/bar", cr: newCertificateRequestWithUsername("bar")}, want: false},
119+
{name: "unrelated", args: args{val: "spiffe://example.com", cr: newCertificateRequestWithUsername("system:serviceaccount:foo-ns:bar")}, want: false},
120+
}
121+
for _, tt := range tests {
122+
t.Run(tt.name, func(t *testing.T) {
123+
got, err := v.Validate(tt.args.val, tt.args.cr)
124+
if tt.wantErr {
125+
assert.Error(t, err)
126+
return
127+
} else {
128+
assert.NoError(t, err)
129+
}
130+
assert.Equal(t, tt.want, got)
131+
})
132+
}
133+
}
134+
135+
func newCertificateRequestWithUsername(username string) cmapi.CertificateRequest {
136+
request := cmapi.CertificateRequest{
137+
Spec: cmapi.CertificateRequestSpec{
138+
Username: username,
139+
},
140+
}
141+
return request
142+
}

0 commit comments

Comments
 (0)