-
Notifications
You must be signed in to change notification settings - Fork 2k
/
approval.go
300 lines (251 loc) · 9.31 KB
/
approval.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/*
Copyright 2021 The cert-manager Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package plugins
import (
"context"
"errors"
"fmt"
admissionv1 "k8s.io/api/admission/v1"
authzv1 "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/client-go/discovery"
"k8s.io/client-go/kubernetes"
authzclient "k8s.io/client-go/kubernetes/typed/authorization/v1"
"github.com/jetstack/cert-manager/pkg/apis/certmanager"
cmapi "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1"
internalcmapi "github.com/jetstack/cert-manager/pkg/internal/apis/certmanager"
"github.com/jetstack/cert-manager/pkg/internal/apis/certmanager/validation/util"
)
// approval is responsible for reviewing whether users attempting to approve or
// deny a CertificateRequest have sufficient permissions to do so.
type approval struct {
scheme *runtime.Scheme
sarclient authzclient.SubjectAccessReviewInterface
discoverclient discovery.DiscoveryInterface
}
type signerResource struct {
// signer resource name
name string
group string
namespaced bool
// name of the object for this signer
signerName string
requestNamespace string
}
func newApproval(scheme *runtime.Scheme) *approval {
return &approval{
scheme: scheme,
}
}
func (a *approval) Init(client kubernetes.Interface) {
a.sarclient = client.AuthorizationV1().SubjectAccessReviews()
a.discoverclient = client.Discovery()
}
// Validate will review whether the client is able to approve or deny the given
// request, if indeed they are attempting to. A SubjectAccessReview will be
// performed if the client is attempting to approve/deny the request. An error
// will be returned if the SubjectAccessReview fails, or if they do not have
// permissions to perform the approval/denial. The request will also fail if
// the referenced signer doesn't exist in this cluster.
func (a *approval) Validate(ctx context.Context, req *admissionv1.AdmissionRequest, oldObj, obj runtime.Object) *field.Error {
// Only perform validation on UPDATE operations
if req.Operation != admissionv1.Update {
return nil
}
// Only Validate over CertificateRequest resources
if req.RequestKind.Group != certmanager.GroupName || req.RequestKind.Kind != cmapi.CertificateRequestKind {
return nil
}
// Error if the clients are not initialised
if a.sarclient == nil || a.discoverclient == nil {
return internalError(errors.New("approval validation not initialised"))
}
gvk := schema.GroupVersionKind{
Group: req.RequestKind.Group,
Version: runtime.APIVersionInternal,
Kind: req.RequestKind.Kind,
}
// Convert the incomming old and new CertificateRequest into the internal
// version. This is so we can process a single type, reglardless of whatever
// CertificateRequest version is in the request.
for _, obj := range []runtime.Object{oldObj, obj} {
internalObj, err := a.scheme.New(gvk)
if err != nil {
return internalError(err)
}
if err := a.scheme.Convert(obj, internalObj, nil); err != nil {
return internalError(err)
}
}
oldCR := oldObj.(*internalcmapi.CertificateRequest)
newCR := obj.(*internalcmapi.CertificateRequest)
// If the request is not for approval, exit early
if !isApprovalRequest(oldCR, newCR) {
return nil
}
// Get the referenced signer signer definition
signer, ok, err := a.signerResource(newCR)
if err != nil {
return internalError(err)
}
if !ok {
return field.Forbidden(field.NewPath("spec.issuerRef"),
fmt.Sprintf("referenced signer resource does not exist: %v", newCR.Spec.IssuerRef))
}
// Construct the signer resource names that permissions should be granted
// for
names := a.signerResourceNames(signer)
// Review whether the approving user has the correct permissions for the
// given signer names
ok, err = a.reviewRequest(ctx, req, names)
if err != nil {
return internalError(err)
}
if !ok {
return field.Forbidden(field.NewPath("status.conditions"),
fmt.Sprintf("user %q does not have permissions to set approved/denied conditions for issuer %v", req.UserInfo.Username, newCR.Spec.IssuerRef))
}
return nil
}
// reviewRequest will perform a SubjectAccessReview with the UserInfo fields of
// the client against the issuer of the CertificateRequest. A client must have
// the "approve" verb, for the resource "signer", at the Cluster scope, for the
// name "<signer-kind>.<signer-group>/[<signer-namespace.]<signer-name>", or
// "<signer-kind>.<signer-group>/*".
func (a *approval) reviewRequest(ctx context.Context, req *admissionv1.AdmissionRequest, names []string) (bool, error) {
extra := make(map[string]authzv1.ExtraValue)
for k, v := range req.UserInfo.Extra {
extra[k] = authzv1.ExtraValue(v)
}
for _, name := range names {
resp, err := a.sarclient.Create(ctx, &authzv1.SubjectAccessReview{
Spec: authzv1.SubjectAccessReviewSpec{
User: req.UserInfo.Username,
Groups: req.UserInfo.Groups,
Extra: extra,
UID: req.UserInfo.UID,
ResourceAttributes: &authzv1.ResourceAttributes{
Group: certmanager.GroupName,
Resource: "signers",
Name: name,
Verb: "approve",
Version: "*",
},
},
}, metav1.CreateOptions{})
if err != nil {
return false, err
}
if resp.Status.Allowed {
return true, nil
}
}
return false, nil
}
// isApprovalRequest will return true if the request is given a new approved or
// denied condition. This check is strictly concerned with these conditions
// being _added_. We do this to reduce the number of SAR calls made, since
// removal or changing of these conditions will be rejected elsewhere in the
// validation chain locally.
func isApprovalRequest(oldCR, newCR *internalcmapi.CertificateRequest) bool {
oldCRApproving := util.GetCertificateRequestCondition(oldCR.Status.Conditions, internalcmapi.CertificateRequestConditionApproved)
newCRApproving := util.GetCertificateRequestCondition(newCR.Status.Conditions, internalcmapi.CertificateRequestConditionApproved)
if oldCRApproving == nil && newCRApproving != nil {
return true
}
oldCRDenying := util.GetCertificateRequestCondition(oldCR.Status.Conditions, internalcmapi.CertificateRequestConditionDenied)
newCRDenying := util.GetCertificateRequestCondition(newCR.Status.Conditions, internalcmapi.CertificateRequestConditionDenied)
if oldCRDenying == nil && newCRDenying != nil {
return true
}
return false
}
// signerResourceNames returns a slice of the signer resource names that this
// signer can be represented as, given the request.
func (a *approval) signerResourceNames(signer *signerResource) []string {
wildcard := fmt.Sprintf("%s.%s/*", signer.name, signer.group)
named := fmt.Sprintf("%s.%s", signer.name, signer.group)
if signer.namespaced {
named = fmt.Sprintf("%s/%s.%s", named, signer.requestNamespace, signer.signerName)
} else {
named = fmt.Sprintf("%s/%s", named, signer.signerName)
}
return []string{wildcard, named}
}
// signerResource returns information about the singer resource in the cluster,
// using the discovery client. Returns false if the signer is not installed in
// the cluster.
func (a *approval) signerResource(cr *internalcmapi.CertificateRequest) (*signerResource, bool, error) {
group := cr.Spec.IssuerRef.Group
if len(group) == 0 {
group = certmanager.GroupName
}
kind := cr.Spec.IssuerRef.Kind
if len(kind) == 0 {
kind = cmapi.IssuerKind
}
// Test for internal signer types and return accordingly
if group == certmanager.GroupName {
switch kind {
case cmapi.IssuerKind:
return &signerResource{
name: "issuers",
group: group,
namespaced: true,
signerName: cr.Spec.IssuerRef.Name,
requestNamespace: cr.Namespace,
}, true, nil
case cmapi.ClusterIssuerKind:
return &signerResource{
name: "clusterissuers",
group: group,
namespaced: false,
signerName: cr.Spec.IssuerRef.Name,
requestNamespace: cr.Namespace,
}, true, nil
}
}
grouplist, err := a.discoverclient.ServerGroups()
if err != nil {
return nil, false, err
}
for _, resourceGroup := range grouplist.Groups {
if group != resourceGroup.Name {
continue
}
for _, version := range resourceGroup.Versions {
resources, err := a.discoverclient.ServerResourcesForGroupVersion(version.GroupVersion)
if err != nil {
return nil, false, err
}
for _, resource := range resources.APIResources {
if resource.Kind == kind {
return &signerResource{
name: resource.Name,
group: group,
namespaced: resource.Namespaced,
requestNamespace: cr.Namespace,
signerName: cr.Spec.IssuerRef.Name,
}, true, nil
}
}
}
}
return nil, false, nil
}
func internalError(err error) *field.Error {
return field.InternalError(field.NewPath("status.conditions"), err)
}