Matthias Andreas Benkard | 832a54e | 2019-01-29 09:27:38 +0100 | [diff] [blame] | 1 | /* |
| 2 | Copyright 2016 The Kubernetes 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 webhook implements the authorizer.Authorizer interface using HTTP webhooks. |
| 18 | package webhook |
| 19 | |
| 20 | import ( |
| 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 | |
| 38 | var ( |
| 39 | groupVersions = []schema.GroupVersion{authorization.SchemeGroupVersion} |
| 40 | ) |
| 41 | |
| 42 | const retryBackoff = 500 * time.Millisecond |
| 43 | |
| 44 | // Ensure Webhook implements the authorizer.Authorizer interface. |
| 45 | var _ authorizer.Authorizer = (*WebhookAuthorizer)(nil) |
| 46 | |
| 47 | type 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 |
| 57 | func 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/. |
| 81 | func 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. |
| 90 | func 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. |
| 148 | func (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 |
| 216 | func (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 | |
| 225 | func 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. |
| 240 | func 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 | |
| 252 | type subjectAccessReviewClient struct { |
| 253 | w *webhook.GenericWebhook |
| 254 | } |
| 255 | |
| 256 | func (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 | } |