1/*
2Copyright 2017 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
17package openstack
18
19import (
20	"fmt"
21	"net/http"
22	"sync"
23	"time"
24
25	"github.com/gophercloud/gophercloud"
26	"github.com/gophercloud/gophercloud/openstack"
27	"k8s.io/klog"
28
29	"k8s.io/apimachinery/pkg/util/net"
30	restclient "k8s.io/client-go/rest"
31)
32
33func init() {
34	if err := restclient.RegisterAuthProviderPlugin("openstack", newOpenstackAuthProvider); err != nil {
35		klog.Fatalf("Failed to register openstack auth plugin: %s", err)
36	}
37}
38
39// DefaultTTLDuration is the time before a token gets expired.
40const DefaultTTLDuration = 10 * time.Minute
41
42// openstackAuthProvider is an authprovider for openstack. this provider reads
43// the environment variables to determine the client identity, and generates a
44// token which will be inserted into the request header later.
45type openstackAuthProvider struct {
46	ttl         time.Duration
47	tokenGetter TokenGetter
48}
49
50// TokenGetter returns a bearer token that can be inserted into request.
51type TokenGetter interface {
52	Token() (string, error)
53}
54
55type tokenGetter struct {
56	authOpt *gophercloud.AuthOptions
57}
58
59// Token creates a token by authenticate with keystone.
60func (t *tokenGetter) Token() (string, error) {
61	var options gophercloud.AuthOptions
62	var err error
63	if t.authOpt == nil {
64		// reads the config from the environment
65		klog.V(4).Info("reading openstack config from the environment variables")
66		options, err = openstack.AuthOptionsFromEnv()
67		if err != nil {
68			return "", fmt.Errorf("failed to read openstack env vars: %s", err)
69		}
70	} else {
71		options = *t.authOpt
72	}
73	client, err := openstack.AuthenticatedClient(options)
74	if err != nil {
75		return "", fmt.Errorf("authentication failed: %s", err)
76	}
77	return client.TokenID, nil
78}
79
80// cachedGetter caches a token until it gets expired, after the expiration, it will
81// generate another token and cache it.
82type cachedGetter struct {
83	mutex       sync.Mutex
84	tokenGetter TokenGetter
85
86	token string
87	born  time.Time
88	ttl   time.Duration
89}
90
91// Token returns the current available token, create a new one if expired.
92func (c *cachedGetter) Token() (string, error) {
93	c.mutex.Lock()
94	defer c.mutex.Unlock()
95
96	var err error
97	// no token or exceeds the TTL
98	if c.token == "" || time.Since(c.born) > c.ttl {
99		c.token, err = c.tokenGetter.Token()
100		if err != nil {
101			return "", fmt.Errorf("failed to get token: %s", err)
102		}
103		c.born = time.Now()
104	}
105	return c.token, nil
106}
107
108// tokenRoundTripper implements the RoundTripper interface: adding the bearer token
109// into the request header.
110type tokenRoundTripper struct {
111	http.RoundTripper
112
113	tokenGetter TokenGetter
114}
115
116var _ net.RoundTripperWrapper = &tokenRoundTripper{}
117
118// RoundTrip adds the bearer token into the request.
119func (t *tokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
120	// if the authorization header already present, use it.
121	if req.Header.Get("Authorization") != "" {
122		return t.RoundTripper.RoundTrip(req)
123	}
124
125	token, err := t.tokenGetter.Token()
126	if err == nil {
127		req.Header.Set("Authorization", "Bearer "+token)
128	} else {
129		klog.V(4).Infof("failed to get token: %s", err)
130	}
131
132	return t.RoundTripper.RoundTrip(req)
133}
134
135func (t *tokenRoundTripper) WrappedRoundTripper() http.RoundTripper { return t.RoundTripper }
136
137// newOpenstackAuthProvider creates an auth provider which works with openstack
138// environment.
139func newOpenstackAuthProvider(_ string, config map[string]string, persister restclient.AuthProviderConfigPersister) (restclient.AuthProvider, error) {
140	var ttlDuration time.Duration
141	var err error
142
143	klog.Warningf("WARNING: in-tree openstack auth plugin is now deprecated. please use the \"client-keystone-auth\" kubectl/client-go credential plugin instead")
144	ttl, found := config["ttl"]
145	if !found {
146		ttlDuration = DefaultTTLDuration
147		// persist to config
148		config["ttl"] = ttlDuration.String()
149		if err = persister.Persist(config); err != nil {
150			return nil, fmt.Errorf("failed to persist config: %s", err)
151		}
152	} else {
153		ttlDuration, err = time.ParseDuration(ttl)
154		if err != nil {
155			return nil, fmt.Errorf("failed to parse ttl config: %s", err)
156		}
157	}
158
159	authOpt := gophercloud.AuthOptions{
160		IdentityEndpoint: config["identityEndpoint"],
161		Username:         config["username"],
162		Password:         config["password"],
163		DomainName:       config["name"],
164		TenantID:         config["tenantId"],
165		TenantName:       config["tenantName"],
166	}
167
168	getter := tokenGetter{}
169	// not empty
170	if (authOpt != gophercloud.AuthOptions{}) {
171		if len(authOpt.IdentityEndpoint) == 0 {
172			return nil, fmt.Errorf("empty %q in the config for openstack auth provider", "identityEndpoint")
173		}
174		getter.authOpt = &authOpt
175	}
176
177	return &openstackAuthProvider{
178		ttl:         ttlDuration,
179		tokenGetter: &getter,
180	}, nil
181}
182
183func (oap *openstackAuthProvider) WrapTransport(rt http.RoundTripper) http.RoundTripper {
184	return &tokenRoundTripper{
185		RoundTripper: rt,
186		tokenGetter: &cachedGetter{
187			tokenGetter: oap.tokenGetter,
188			ttl:         oap.ttl,
189		},
190	}
191}
192
193func (oap *openstackAuthProvider) Login() error { return nil }
194