-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
url.go
351 lines (320 loc) · 12.9 KB
/
url.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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
/*
Copyright 2020 Gravitational, Inc.
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 proxy
import (
"bytes"
"io"
"net/http"
"path"
"strings"
"github.com/gravitational/trace"
"k8s.io/apimachinery/pkg/runtime/serializer"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/lib/kube/proxy/responsewriters"
)
type apiResource struct {
apiGroup string
apiGroupVersion string
namespace string
resourceKind string
resourceName string
skipEvent bool
isWatch bool
}
// parseResourcePath does best-effort parsing of a Kubernetes API request path.
// All fields of the returned apiResource may be empty.
func parseResourcePath(p string) apiResource {
// Kubernetes API reference: https://kubernetes.io/docs/reference/kubernetes-api/
// Let's try to parse this. Here be dragons!
//
// URLs have a prefix that defines an "API group":
// - /api/v1/ - the special "core" API group (e.g. pods, secrets, etc. belong here)
// - /apis/{group}/{version} - the other properly named groups (e.g. apps/v1 or rbac.authorization.k8s.io/v1beta1)
//
// After the prefix, we have the resource info:
// - /namespaces/{namespace}/{resource kind}/{resource name} for namespaced resources
// - turns out, namespace is optional when you query across all
// namespaces (e.g. /api/v1/pods to get pods in all namespaces)
// - /{resource kind}/{resource name} for cluster-scoped resources (e.g. namespaces or nodes)
//
// If {resource name} is missing, the request refers to all resources of
// that kind (e.g. list all pods).
//
// There can be more items after {resource name} (a "subresource"), like
// pods/foo/exec, but the depth is arbitrary (e.g.
// /api/v1/namespaces/{namespace}/pods/{name}/proxy/{path})
//
// And the cherry on top - watch endpoints, e.g.
// /api/v1/watch/namespaces/{namespace}/pods/{name}
// for live updates on resources (specific resources or all of one kind)
var r apiResource
// Clean up the path and make it absolute.
p = path.Clean(p)
if !path.IsAbs(p) {
p = "/" + p
}
parts := strings.Split(p, "/")
switch {
// Core API group has a "special" URL prefix /api/v1/.
case len(parts) >= 3 && parts[1] == "api" && parts[2] == "v1":
r.apiGroup = "core"
r.apiGroupVersion = parts[2]
parts = parts[3:]
// Other API groups have URL prefix /apis/{group}/{version}.
case len(parts) >= 4 && parts[1] == "apis":
r.apiGroup, r.apiGroupVersion = parts[2], parts[3]
parts = parts[4:]
case len(parts) >= 2 && (parts[1] == "api" || parts[1] == "apis"):
// /api or /apis.
// This is part of API discovery. Don't emit to audit log to reduce
// noise.
r.skipEvent = true
return r
default:
// Doesn't look like a k8s API path, return empty result.
return r
}
// Watch API endpoints have an extra /watch/ prefix. For now, silently
// strip it from our result.
if len(parts) > 0 && parts[0] == "watch" {
r.isWatch = true
parts = parts[1:]
}
switch len(parts) {
case 0:
// e.g. /apis/apps/v1
// This is part of API discovery. Don't emit to audit log to reduce
// noise.
r.skipEvent = true
return r
case 1:
// e.g. /api/v1/pods - list pods in all namespaces
r.resourceKind = parts[0]
case 2:
// e.g. /api/v1/clusterroles/{name} - read a cluster-level resource
r.resourceKind = parts[0]
r.resourceName = parts[1]
case 3:
if parts[0] == "namespaces" {
// e.g. /api/v1/namespaces/{namespace}/pods - list pods in a
// specific namespace
r.namespace = parts[1]
r.resourceKind = parts[2]
} else {
// e.g. /apis/apiregistration.k8s.io/v1/apiservices/{name}/status
kind := append([]string{parts[0]}, parts[2:]...)
r.resourceKind = strings.Join(kind, "/")
r.resourceName = parts[1]
}
default:
// e.g. /api/v1/namespaces/{namespace}/pods/{name} - get a specific pod
// or /api/v1/namespaces/{namespace}/pods/{name}/exec - exec command in a pod
if parts[0] == "namespaces" {
r.namespace = parts[1]
kind := append([]string{parts[2]}, parts[4:]...)
r.resourceKind = strings.Join(kind, "/")
r.resourceName = parts[3]
} else {
// e.g. /api/v1/nodes/{name}/proxy/{path}
kind := append([]string{parts[0]}, parts[2:]...)
r.resourceKind = strings.Join(kind, "/")
r.resourceName = parts[1]
}
}
return r
}
func (r apiResource) populateEvent(e *apievents.KubeRequest) {
e.ResourceAPIGroup = path.Join(r.apiGroup, r.apiGroupVersion)
e.ResourceNamespace = r.namespace
e.ResourceKind = r.resourceKind
e.ResourceName = r.resourceName
}
// allowedResourcesKey is a key used to identify a resource in the allowedResources map.
type allowedResourcesKey struct {
apiGroup string
resourceKind string
}
type rbacSupportedResources map[allowedResourcesKey]string
// getResourceWithKey returns the teleport resource kind for a given resource key if
// it exists, otherwise returns an empty string.
func (r rbacSupportedResources) getResourceWithKey(k allowedResourcesKey) string {
if k.apiGroup == "" {
k.apiGroup = "core"
}
return r[k]
}
func (r rbacSupportedResources) getTeleportResourceKindFromAPIResource(api apiResource) (string, bool) {
resource := getResourceFromAPIResource(api.resourceKind)
resourceType, ok := r[allowedResourcesKey{apiGroup: api.apiGroup, resourceKind: resource}]
return resourceType, ok
}
// defaultRBACResources is a map of supported resources and their corresponding
// teleport resource kind for the purpose of resource rbac.
var defaultRBACResources = rbacSupportedResources{
{apiGroup: "core", resourceKind: "pods"}: types.KindKubePod,
{apiGroup: "core", resourceKind: "secrets"}: types.KindKubeSecret,
{apiGroup: "core", resourceKind: "configmaps"}: types.KindKubeConfigmap,
{apiGroup: "core", resourceKind: "namespaces"}: types.KindKubeNamespace,
{apiGroup: "core", resourceKind: "services"}: types.KindKubeService,
{apiGroup: "core", resourceKind: "endpoints"}: types.KindKubeService,
{apiGroup: "core", resourceKind: "serviceaccounts"}: types.KindKubeServiceAccount,
{apiGroup: "core", resourceKind: "nodes"}: types.KindKubeNode,
{apiGroup: "core", resourceKind: "persistentvolumes"}: types.KindKubePersistentVolume,
{apiGroup: "core", resourceKind: "persistentvolumeclaims"}: types.KindKubePersistentVolumeClaim,
{apiGroup: "apps", resourceKind: "deployments"}: types.KindKubeDeployment,
{apiGroup: "apps", resourceKind: "replicasets"}: types.KindKubeReplicaSet,
{apiGroup: "apps", resourceKind: "statefulsets"}: types.KindKubeStatefulset,
{apiGroup: "apps", resourceKind: "daemonsets"}: types.KindKubeDaemonSet,
{apiGroup: "rbac.authorization.k8s.io", resourceKind: "clusterroles"}: types.KindKubeClusterRole,
{apiGroup: "rbac.authorization.k8s.io", resourceKind: "roles"}: types.KindKubeRole,
{apiGroup: "rbac.authorization.k8s.io", resourceKind: "clusterrolebindings"}: types.KindKubeClusterRoleBinding,
{apiGroup: "rbac.authorization.k8s.io", resourceKind: "rolebindings"}: types.KindKubeRoleBinding,
{apiGroup: "batch", resourceKind: "cronjobs"}: types.KindKubeCronjob,
{apiGroup: "batch", resourceKind: "jobs"}: types.KindKubeJob,
{apiGroup: "certificates.k8s.io", resourceKind: "certificatesigningrequests"}: types.KindKubeCertificateSigningRequest,
{apiGroup: "networking.k8s.io", resourceKind: "ingresses"}: types.KindKubeIngress,
{apiGroup: "extensions", resourceKind: "deployments"}: types.KindKubeDeployment,
{apiGroup: "extensions", resourceKind: "replicasets"}: types.KindKubeReplicaSet,
{apiGroup: "extensions", resourceKind: "daemonsets"}: types.KindKubeDaemonSet,
{apiGroup: "extensions", resourceKind: "ingresses"}: types.KindKubeIngress,
}
// getResourceFromRequest returns a KubernetesResource if the user tried to access
// a specific endpoint that Teleport support resource filtering. Otherwise, returns nil.
func getResourceFromRequest(req *http.Request, kubeDetails *kubeDetails) (*types.KubernetesResource, apiResource, error) {
apiResource := parseResourcePath(req.URL.Path)
verb := apiResource.getVerb(req)
if kubeDetails == nil {
return nil, apiResource, nil
}
codecFactory, rbacSupportedTypes, err := kubeDetails.getClusterSupportedResources()
if err != nil {
return nil, apiResource, trace.Wrap(err)
}
resourceType, ok := rbacSupportedTypes.getTeleportResourceKindFromAPIResource(apiResource)
switch {
case !ok:
// if the resource is not supported, return nil.
return nil, apiResource, nil
case apiResource.resourceName == "" && verb != types.KubeVerbCreate:
// if the resource is supported but the resource name is not present and not a create request,
// return nil because it's a list request.
return nil, apiResource, nil
case apiResource.resourceName == "" && verb == types.KubeVerbCreate:
// If the request is a create request, extract the resource name from the request body.
var err error
if apiResource.resourceName, err = extractResourceNameFromPostRequest(req, codecFactory); err != nil {
return nil, apiResource, trace.Wrap(err)
}
}
return &types.KubernetesResource{
Kind: resourceType,
Namespace: apiResource.namespace,
Name: apiResource.resourceName,
Verbs: []string{verb},
}, apiResource, nil
}
// extractResourceNameFromPostRequest extracts the resource name from a POST body.
// It reads the full body - required because data can be proto encoded -
// and decodes it into a Kubernetes object. It then extracts the resource name
// from the object.
// The body is then reset to the original request body using a new buffer.
func extractResourceNameFromPostRequest(req *http.Request, codecs *serializer.CodecFactory) (string, error) {
if req.Body == nil {
return "", trace.BadParameter("request body is empty")
}
negotiator := newClientNegotiator(codecs)
_, decoder, err := newEncoderAndDecoderForContentType(
responsewriters.GetContentTypeHeader(req.Header),
negotiator,
)
if err != nil {
return "", trace.Wrap(err)
}
newBody := bytes.NewBuffer(make([]byte, 0, 2048))
if _, err := io.Copy(newBody, req.Body); err != nil {
return "", trace.Wrap(err)
}
if err := req.Body.Close(); err != nil {
return "", trace.Wrap(err)
}
req.Body = io.NopCloser(newBody)
// decode memory rw body.
obj, err := decodeAndSetGVK(decoder, newBody.Bytes())
if err != nil {
return "", trace.Wrap(err)
}
namer, ok := obj.(kubeObjectInterface)
if !ok {
return "", trace.BadParameter("object %T does not implement kubeObjectInterface", obj)
}
return namer.GetName(), nil
}
// getResourceFromAPIResource returns the resource kind from the api resource.
// If the resource kind contains sub resources (e.g. pods/exec), it returns the
// resource kind without the subresource.
func getResourceFromAPIResource(resourceKind string) string {
if idx := strings.Index(resourceKind, "/"); idx != -1 {
return resourceKind[:idx]
}
return resourceKind
}
// isKubeWatchRequest returns true if the request is a watch request.
func isKubeWatchRequest(req *http.Request, r apiResource) bool {
if values := req.URL.Query()["watch"]; len(values) > 0 {
switch strings.ToLower(values[0]) {
case "false", "0":
default:
return true
}
}
return r.isWatch
}
func (r apiResource) getVerb(req *http.Request) string {
verb := ""
isWatch := isKubeWatchRequest(req, r)
switch r.resourceKind {
case "pods/exec", "pods/attach":
verb = types.KubeVerbExec
case "pods/portforward":
verb = types.KubeVerbPortForward
default:
switch req.Method {
case http.MethodPost:
verb = types.KubeVerbCreate
case http.MethodGet, http.MethodHead, http.MethodOptions:
switch {
case isWatch:
return types.KubeVerbWatch
case r.resourceName == "":
return types.KubeVerbList
default:
return types.KubeVerbGet
}
case http.MethodPut:
verb = types.KubeVerbUpdate
case http.MethodPatch:
verb = types.KubeVerbPatch
case http.MethodDelete:
switch {
case r.resourceName != "":
verb = types.KubeVerbDelete
default:
verb = types.KubeVerbDeleteCollection
}
default:
verb = ""
}
}
return verb
}