blob: 83157c48c862fa47521c41c8adcf8f7986a6ad9e [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2016 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8 http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17// Package webhook implements the authorizer.Authorizer interface using HTTP webhooks.
18package webhook
19
20import (
21 "encoding/json"
22 "fmt"
23 "time"
24
25 "github.com/golang/glog"
26
27 authorization "k8s.io/api/authorization/v1beta1"
28 "k8s.io/apimachinery/pkg/runtime"
29 "k8s.io/apimachinery/pkg/runtime/schema"
30 "k8s.io/apimachinery/pkg/util/cache"
31 "k8s.io/apiserver/pkg/authentication/user"
32 "k8s.io/apiserver/pkg/authorization/authorizer"
33 "k8s.io/apiserver/pkg/util/webhook"
34 "k8s.io/client-go/kubernetes/scheme"
35 authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1"
36)
37
38var (
39 groupVersions = []schema.GroupVersion{authorization.SchemeGroupVersion}
40)
41
42const retryBackoff = 500 * time.Millisecond
43
44// Ensure Webhook implements the authorizer.Authorizer interface.
45var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil)
46
47type WebhookAuthorizer struct {
48 subjectAccessReview authorizationclient.SubjectAccessReviewInterface
49 responseCache *cache.LRUExpireCache
50 authorizedTTL time.Duration
51 unauthorizedTTL time.Duration
52 initialBackoff time.Duration
53 decisionOnError authorizer.Decision
54}
55
56// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
57func NewFromInterface(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
58 return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff)
59}
60
61// New creates a new WebhookAuthorizer from the provided kubeconfig file.
62//
63// The config's cluster field is used to refer to the remote service, user refers to the returned authorizer.
64//
65// # clusters refers to the remote service.
66// clusters:
67// - name: name-of-remote-authz-service
68// cluster:
69// certificate-authority: /path/to/ca.pem # CA for verifying the remote service.
70// server: https://authz.example.com/authorize # URL of remote service to query. Must use 'https'.
71//
72// # users refers to the API server's webhook configuration.
73// users:
74// - name: name-of-api-server
75// user:
76// client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
77// client-key: /path/to/key.pem # key matching the cert
78//
79// For additional HTTP configuration, refer to the kubeconfig documentation
80// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
81func New(kubeConfigFile string, authorizedTTL, unauthorizedTTL time.Duration) (*WebhookAuthorizer, error) {
82 subjectAccessReview, err := subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile)
83 if err != nil {
84 return nil, err
85 }
86 return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff)
87}
88
89// newWithBackoff allows tests to skip the sleep.
90func newWithBackoff(subjectAccessReview authorizationclient.SubjectAccessReviewInterface, authorizedTTL, unauthorizedTTL, initialBackoff time.Duration) (*WebhookAuthorizer, error) {
91 return &WebhookAuthorizer{
92 subjectAccessReview: subjectAccessReview,
93 responseCache: cache.NewLRUExpireCache(1024),
94 authorizedTTL: authorizedTTL,
95 unauthorizedTTL: unauthorizedTTL,
96 initialBackoff: initialBackoff,
97 decisionOnError: authorizer.DecisionNoOpinion,
98 }, nil
99}
100
101// Authorize makes a REST request to the remote service describing the attempted action as a JSON
102// serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
103// provided below.
104//
105// {
106// "apiVersion": "authorization.k8s.io/v1beta1",
107// "kind": "SubjectAccessReview",
108// "spec": {
109// "resourceAttributes": {
110// "namespace": "kittensandponies",
111// "verb": "GET",
112// "group": "group3",
113// "resource": "pods"
114// },
115// "user": "jane",
116// "group": [
117// "group1",
118// "group2"
119// ]
120// }
121// }
122//
123// The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
124// disallow access. A permissive response would return:
125//
126// {
127// "apiVersion": "authorization.k8s.io/v1beta1",
128// "kind": "SubjectAccessReview",
129// "status": {
130// "allowed": true
131// }
132// }
133//
134// To disallow access, the remote service would return:
135//
136// {
137// "apiVersion": "authorization.k8s.io/v1beta1",
138// "kind": "SubjectAccessReview",
139// "status": {
140// "allowed": false,
141// "reason": "user does not have read access to the namespace"
142// }
143// }
144//
145// TODO(mikedanese): We should eventually support failing closed when we
146// encounter an error. We are failing open now to preserve backwards compatible
147// behavior.
148func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
149 r := &authorization.SubjectAccessReview{}
150 if user := attr.GetUser(); user != nil {
151 r.Spec = authorization.SubjectAccessReviewSpec{
152 User: user.GetName(),
153 UID: user.GetUID(),
154 Groups: user.GetGroups(),
155 Extra: convertToSARExtra(user.GetExtra()),
156 }
157 }
158
159 if attr.IsResourceRequest() {
160 r.Spec.ResourceAttributes = &authorization.ResourceAttributes{
161 Namespace: attr.GetNamespace(),
162 Verb: attr.GetVerb(),
163 Group: attr.GetAPIGroup(),
164 Version: attr.GetAPIVersion(),
165 Resource: attr.GetResource(),
166 Subresource: attr.GetSubresource(),
167 Name: attr.GetName(),
168 }
169 } else {
170 r.Spec.NonResourceAttributes = &authorization.NonResourceAttributes{
171 Path: attr.GetPath(),
172 Verb: attr.GetVerb(),
173 }
174 }
175 key, err := json.Marshal(r.Spec)
176 if err != nil {
177 return w.decisionOnError, "", err
178 }
179 if entry, ok := w.responseCache.Get(string(key)); ok {
180 r.Status = entry.(authorization.SubjectAccessReviewStatus)
181 } else {
182 var (
183 result *authorization.SubjectAccessReview
184 err error
185 )
186 webhook.WithExponentialBackoff(w.initialBackoff, func() error {
187 result, err = w.subjectAccessReview.Create(r)
188 return err
189 })
190 if err != nil {
191 // An error here indicates bad configuration or an outage. Log for debugging.
192 glog.Errorf("Failed to make webhook authorizer request: %v", err)
193 return w.decisionOnError, "", err
194 }
195 r.Status = result.Status
196 if r.Status.Allowed {
197 w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
198 } else {
199 w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
200 }
201 }
202 switch {
203 case r.Status.Denied && r.Status.Allowed:
204 return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
205 case r.Status.Denied:
206 return authorizer.DecisionDeny, r.Status.Reason, nil
207 case r.Status.Allowed:
208 return authorizer.DecisionAllow, r.Status.Reason, nil
209 default:
210 return authorizer.DecisionNoOpinion, r.Status.Reason, nil
211 }
212
213}
214
215//TODO: need to finish the method to get the rules when using webhook mode
216func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
217 var (
218 resourceRules []authorizer.ResourceRuleInfo
219 nonResourceRules []authorizer.NonResourceRuleInfo
220 )
221 incomplete := true
222 return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
223}
224
225func convertToSARExtra(extra map[string][]string) map[string]authorization.ExtraValue {
226 if extra == nil {
227 return nil
228 }
229 ret := map[string]authorization.ExtraValue{}
230 for k, v := range extra {
231 ret[k] = authorization.ExtraValue(v)
232 }
233
234 return ret
235}
236
237// subjectAccessReviewInterfaceFromKubeconfig builds a client from the specified kubeconfig file,
238// and returns a SubjectAccessReviewInterface that uses that client. Note that the client submits SubjectAccessReview
239// requests to the exact path specified in the kubeconfig file, so arbitrary non-API servers can be targeted.
240func subjectAccessReviewInterfaceFromKubeconfig(kubeConfigFile string) (authorizationclient.SubjectAccessReviewInterface, error) {
241 localScheme := runtime.NewScheme()
242 scheme.AddToScheme(localScheme)
243 localScheme.SetVersionPriority(groupVersions...)
244
245 gw, err := webhook.NewGenericWebhook(localScheme, scheme.Codecs, kubeConfigFile, groupVersions, 0)
246 if err != nil {
247 return nil, err
248 }
249 return &subjectAccessReviewClient{gw}, nil
250}
251
252type subjectAccessReviewClient struct {
253 w *webhook.GenericWebhook
254}
255
256func (t *subjectAccessReviewClient) Create(subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
257 result := &authorization.SubjectAccessReview{}
258 err := t.w.RestClient.Post().Body(subjectAccessReview).Do().Into(result)
259 return result, err
260}