1package openstack
2
3import (
4	"fmt"
5	"reflect"
6	"strings"
7
8	"github.com/gophercloud/gophercloud"
9	tokens2 "github.com/gophercloud/gophercloud/openstack/identity/v2/tokens"
10	"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/ec2tokens"
11	"github.com/gophercloud/gophercloud/openstack/identity/v3/extensions/oauth1"
12	tokens3 "github.com/gophercloud/gophercloud/openstack/identity/v3/tokens"
13	"github.com/gophercloud/gophercloud/openstack/utils"
14)
15
16const (
17	// v2 represents Keystone v2.
18	// It should never increase beyond 2.0.
19	v2 = "v2.0"
20
21	// v3 represents Keystone v3.
22	// The version can be anything from v3 to v3.x.
23	v3 = "v3"
24)
25
26/*
27NewClient prepares an unauthenticated ProviderClient instance.
28Most users will probably prefer using the AuthenticatedClient function
29instead.
30
31This is useful if you wish to explicitly control the version of the identity
32service that's used for authentication explicitly, for example.
33
34A basic example of using this would be:
35
36	ao, err := openstack.AuthOptionsFromEnv()
37	provider, err := openstack.NewClient(ao.IdentityEndpoint)
38	client, err := openstack.NewIdentityV3(provider, gophercloud.EndpointOpts{})
39*/
40func NewClient(endpoint string) (*gophercloud.ProviderClient, error) {
41	base, err := utils.BaseEndpoint(endpoint)
42	if err != nil {
43		return nil, err
44	}
45
46	endpoint = gophercloud.NormalizeURL(endpoint)
47	base = gophercloud.NormalizeURL(base)
48
49	p := new(gophercloud.ProviderClient)
50	p.IdentityBase = base
51	p.IdentityEndpoint = endpoint
52	p.UseTokenLock()
53
54	return p, nil
55}
56
57/*
58AuthenticatedClient logs in to an OpenStack cloud found at the identity endpoint
59specified by the options, acquires a token, and returns a Provider Client
60instance that's ready to operate.
61
62If the full path to a versioned identity endpoint was specified  (example:
63http://example.com:5000/v3), that path will be used as the endpoint to query.
64
65If a versionless endpoint was specified (example: http://example.com:5000/),
66the endpoint will be queried to determine which versions of the identity service
67are available, then chooses the most recent or most supported version.
68
69Example:
70
71	ao, err := openstack.AuthOptionsFromEnv()
72	provider, err := openstack.AuthenticatedClient(ao)
73	client, err := openstack.NewNetworkV2(provider, gophercloud.EndpointOpts{
74		Region: os.Getenv("OS_REGION_NAME"),
75	})
76*/
77func AuthenticatedClient(options gophercloud.AuthOptions) (*gophercloud.ProviderClient, error) {
78	client, err := NewClient(options.IdentityEndpoint)
79	if err != nil {
80		return nil, err
81	}
82
83	err = Authenticate(client, options)
84	if err != nil {
85		return nil, err
86	}
87	return client, nil
88}
89
90// Authenticate or re-authenticate against the most recent identity service
91// supported at the provided endpoint.
92func Authenticate(client *gophercloud.ProviderClient, options gophercloud.AuthOptions) error {
93	versions := []*utils.Version{
94		{ID: v2, Priority: 20, Suffix: "/v2.0/"},
95		{ID: v3, Priority: 30, Suffix: "/v3/"},
96	}
97
98	chosen, endpoint, err := utils.ChooseVersion(client, versions)
99	if err != nil {
100		return err
101	}
102
103	switch chosen.ID {
104	case v2:
105		return v2auth(client, endpoint, options, gophercloud.EndpointOpts{})
106	case v3:
107		return v3auth(client, endpoint, &options, gophercloud.EndpointOpts{})
108	default:
109		// The switch statement must be out of date from the versions list.
110		return fmt.Errorf("Unrecognized identity version: %s", chosen.ID)
111	}
112}
113
114// AuthenticateV2 explicitly authenticates against the identity v2 endpoint.
115func AuthenticateV2(client *gophercloud.ProviderClient, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error {
116	return v2auth(client, "", options, eo)
117}
118
119func v2auth(client *gophercloud.ProviderClient, endpoint string, options gophercloud.AuthOptions, eo gophercloud.EndpointOpts) error {
120	v2Client, err := NewIdentityV2(client, eo)
121	if err != nil {
122		return err
123	}
124
125	if endpoint != "" {
126		v2Client.Endpoint = endpoint
127	}
128
129	v2Opts := tokens2.AuthOptions{
130		IdentityEndpoint: options.IdentityEndpoint,
131		Username:         options.Username,
132		Password:         options.Password,
133		TenantID:         options.TenantID,
134		TenantName:       options.TenantName,
135		AllowReauth:      options.AllowReauth,
136		TokenID:          options.TokenID,
137	}
138
139	result := tokens2.Create(v2Client, v2Opts)
140
141	err = client.SetTokenAndAuthResult(result)
142	if err != nil {
143		return err
144	}
145
146	catalog, err := result.ExtractServiceCatalog()
147	if err != nil {
148		return err
149	}
150
151	if options.AllowReauth {
152		// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
153		// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
154		// this should retry authentication only once
155		tac := *client
156		tac.SetThrowaway(true)
157		tac.ReauthFunc = nil
158		tac.SetTokenAndAuthResult(nil)
159		tao := options
160		tao.AllowReauth = false
161		client.ReauthFunc = func() error {
162			err := v2auth(&tac, endpoint, tao, eo)
163			if err != nil {
164				return err
165			}
166			client.CopyTokenFrom(&tac)
167			return nil
168		}
169	}
170	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
171		return V2EndpointURL(catalog, opts)
172	}
173
174	return nil
175}
176
177// AuthenticateV3 explicitly authenticates against the identity v3 service.
178func AuthenticateV3(client *gophercloud.ProviderClient, options tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
179	return v3auth(client, "", options, eo)
180}
181
182func v3auth(client *gophercloud.ProviderClient, endpoint string, opts tokens3.AuthOptionsBuilder, eo gophercloud.EndpointOpts) error {
183	// Override the generated service endpoint with the one returned by the version endpoint.
184	v3Client, err := NewIdentityV3(client, eo)
185	if err != nil {
186		return err
187	}
188
189	if endpoint != "" {
190		v3Client.Endpoint = endpoint
191	}
192
193	var catalog *tokens3.ServiceCatalog
194
195	var tokenID string
196	// passthroughToken allows to passthrough the token without a scope
197	var passthroughToken bool
198	switch v := opts.(type) {
199	case *gophercloud.AuthOptions:
200		tokenID = v.TokenID
201		passthroughToken = (v.Scope == nil || *v.Scope == gophercloud.AuthScope{})
202	case *tokens3.AuthOptions:
203		tokenID = v.TokenID
204		passthroughToken = (v.Scope == tokens3.Scope{})
205	}
206
207	if tokenID != "" && passthroughToken {
208		// passing through the token ID without requesting a new scope
209		if opts.CanReauth() {
210			return fmt.Errorf("cannot use AllowReauth, when the token ID is defined and auth scope is not set")
211		}
212
213		v3Client.SetToken(tokenID)
214		result := tokens3.Get(v3Client, tokenID)
215		if result.Err != nil {
216			return result.Err
217		}
218
219		err = client.SetTokenAndAuthResult(result)
220		if err != nil {
221			return err
222		}
223
224		catalog, err = result.ExtractServiceCatalog()
225		if err != nil {
226			return err
227		}
228	} else {
229		var result tokens3.CreateResult
230		switch opts.(type) {
231		case *ec2tokens.AuthOptions:
232			result = ec2tokens.Create(v3Client, opts)
233		case *oauth1.AuthOptions:
234			result = oauth1.Create(v3Client, opts)
235		default:
236			result = tokens3.Create(v3Client, opts)
237		}
238
239		err = client.SetTokenAndAuthResult(result)
240		if err != nil {
241			return err
242		}
243
244		catalog, err = result.ExtractServiceCatalog()
245		if err != nil {
246			return err
247		}
248	}
249
250	if opts.CanReauth() {
251		// here we're creating a throw-away client (tac). it's a copy of the user's provider client, but
252		// with the token and reauth func zeroed out. combined with setting `AllowReauth` to `false`,
253		// this should retry authentication only once
254		tac := *client
255		tac.SetThrowaway(true)
256		tac.ReauthFunc = nil
257		tac.SetTokenAndAuthResult(nil)
258		var tao tokens3.AuthOptionsBuilder
259		switch ot := opts.(type) {
260		case *gophercloud.AuthOptions:
261			o := *ot
262			o.AllowReauth = false
263			tao = &o
264		case *tokens3.AuthOptions:
265			o := *ot
266			o.AllowReauth = false
267			tao = &o
268		case *ec2tokens.AuthOptions:
269			o := *ot
270			o.AllowReauth = false
271			tao = &o
272		case *oauth1.AuthOptions:
273			o := *ot
274			o.AllowReauth = false
275			tao = &o
276		default:
277			tao = opts
278		}
279		client.ReauthFunc = func() error {
280			err := v3auth(&tac, endpoint, tao, eo)
281			if err != nil {
282				return err
283			}
284			client.CopyTokenFrom(&tac)
285			return nil
286		}
287	}
288	client.EndpointLocator = func(opts gophercloud.EndpointOpts) (string, error) {
289		return V3EndpointURL(catalog, opts)
290	}
291
292	return nil
293}
294
295// NewIdentityV2 creates a ServiceClient that may be used to interact with the
296// v2 identity service.
297func NewIdentityV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
298	endpoint := client.IdentityBase + "v2.0/"
299	clientType := "identity"
300	var err error
301	if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
302		eo.ApplyDefaults(clientType)
303		endpoint, err = client.EndpointLocator(eo)
304		if err != nil {
305			return nil, err
306		}
307	}
308
309	return &gophercloud.ServiceClient{
310		ProviderClient: client,
311		Endpoint:       endpoint,
312		Type:           clientType,
313	}, nil
314}
315
316// NewIdentityV3 creates a ServiceClient that may be used to access the v3
317// identity service.
318func NewIdentityV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
319	endpoint := client.IdentityBase + "v3/"
320	clientType := "identity"
321	var err error
322	if !reflect.DeepEqual(eo, gophercloud.EndpointOpts{}) {
323		eo.ApplyDefaults(clientType)
324		endpoint, err = client.EndpointLocator(eo)
325		if err != nil {
326			return nil, err
327		}
328	}
329
330	// Ensure endpoint still has a suffix of v3.
331	// This is because EndpointLocator might have found a versionless
332	// endpoint or the published endpoint is still /v2.0. In both
333	// cases, we need to fix the endpoint to point to /v3.
334	base, err := utils.BaseEndpoint(endpoint)
335	if err != nil {
336		return nil, err
337	}
338
339	base = gophercloud.NormalizeURL(base)
340
341	endpoint = base + "v3/"
342
343	return &gophercloud.ServiceClient{
344		ProviderClient: client,
345		Endpoint:       endpoint,
346		Type:           clientType,
347	}, nil
348}
349
350func initClientOpts(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts, clientType string) (*gophercloud.ServiceClient, error) {
351	sc := new(gophercloud.ServiceClient)
352	eo.ApplyDefaults(clientType)
353	url, err := client.EndpointLocator(eo)
354	if err != nil {
355		return sc, err
356	}
357	sc.ProviderClient = client
358	sc.Endpoint = url
359	sc.Type = clientType
360	return sc, nil
361}
362
363// NewBareMetalV1 creates a ServiceClient that may be used with the v1
364// bare metal package.
365func NewBareMetalV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
366	return initClientOpts(client, eo, "baremetal")
367}
368
369// NewBareMetalIntrospectionV1 creates a ServiceClient that may be used with the v1
370// bare metal introspection package.
371func NewBareMetalIntrospectionV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
372	return initClientOpts(client, eo, "baremetal-inspector")
373}
374
375// NewObjectStorageV1 creates a ServiceClient that may be used with the v1
376// object storage package.
377func NewObjectStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
378	return initClientOpts(client, eo, "object-store")
379}
380
381// NewComputeV2 creates a ServiceClient that may be used with the v2 compute
382// package.
383func NewComputeV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
384	return initClientOpts(client, eo, "compute")
385}
386
387// NewNetworkV2 creates a ServiceClient that may be used with the v2 network
388// package.
389func NewNetworkV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
390	sc, err := initClientOpts(client, eo, "network")
391	sc.ResourceBase = sc.Endpoint + "v2.0/"
392	return sc, err
393}
394
395// NewBlockStorageV1 creates a ServiceClient that may be used to access the v1
396// block storage service.
397func NewBlockStorageV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
398	return initClientOpts(client, eo, "volume")
399}
400
401// NewBlockStorageV2 creates a ServiceClient that may be used to access the v2
402// block storage service.
403func NewBlockStorageV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
404	return initClientOpts(client, eo, "volumev2")
405}
406
407// NewBlockStorageV3 creates a ServiceClient that may be used to access the v3 block storage service.
408func NewBlockStorageV3(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
409	return initClientOpts(client, eo, "volumev3")
410}
411
412// NewSharedFileSystemV2 creates a ServiceClient that may be used to access the v2 shared file system service.
413func NewSharedFileSystemV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
414	return initClientOpts(client, eo, "sharev2")
415}
416
417// NewCDNV1 creates a ServiceClient that may be used to access the OpenStack v1
418// CDN service.
419func NewCDNV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
420	return initClientOpts(client, eo, "cdn")
421}
422
423// NewOrchestrationV1 creates a ServiceClient that may be used to access the v1
424// orchestration service.
425func NewOrchestrationV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
426	return initClientOpts(client, eo, "orchestration")
427}
428
429// NewDBV1 creates a ServiceClient that may be used to access the v1 DB service.
430func NewDBV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
431	return initClientOpts(client, eo, "database")
432}
433
434// NewDNSV2 creates a ServiceClient that may be used to access the v2 DNS
435// service.
436func NewDNSV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
437	sc, err := initClientOpts(client, eo, "dns")
438	sc.ResourceBase = sc.Endpoint + "v2/"
439	return sc, err
440}
441
442// NewImageServiceV2 creates a ServiceClient that may be used to access the v2
443// image service.
444func NewImageServiceV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
445	sc, err := initClientOpts(client, eo, "image")
446	sc.ResourceBase = sc.Endpoint + "v2/"
447	return sc, err
448}
449
450// NewLoadBalancerV2 creates a ServiceClient that may be used to access the v2
451// load balancer service.
452func NewLoadBalancerV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
453	sc, err := initClientOpts(client, eo, "load-balancer")
454
455	// Fixes edge case having an OpenStack lb endpoint with trailing version number.
456	endpoint := strings.Replace(sc.Endpoint, "v2.0/", "", -1)
457
458	sc.ResourceBase = endpoint + "v2.0/"
459	return sc, err
460}
461
462// NewClusteringV1 creates a ServiceClient that may be used with the v1 clustering
463// package.
464func NewClusteringV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
465	return initClientOpts(client, eo, "clustering")
466}
467
468// NewMessagingV2 creates a ServiceClient that may be used with the v2 messaging
469// service.
470func NewMessagingV2(client *gophercloud.ProviderClient, clientID string, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
471	sc, err := initClientOpts(client, eo, "messaging")
472	sc.MoreHeaders = map[string]string{"Client-ID": clientID}
473	return sc, err
474}
475
476// NewContainerV1 creates a ServiceClient that may be used with v1 container package
477func NewContainerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
478	return initClientOpts(client, eo, "container")
479}
480
481// NewKeyManagerV1 creates a ServiceClient that may be used with the v1 key
482// manager service.
483func NewKeyManagerV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
484	sc, err := initClientOpts(client, eo, "key-manager")
485	sc.ResourceBase = sc.Endpoint + "v1/"
486	return sc, err
487}
488
489// NewContainerInfraV1 creates a ServiceClient that may be used with the v1 container infra management
490// package.
491func NewContainerInfraV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
492	return initClientOpts(client, eo, "container-infra")
493}
494
495// NewWorkflowV2 creates a ServiceClient that may be used with the v2 workflow management package.
496func NewWorkflowV2(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
497	return initClientOpts(client, eo, "workflowv2")
498}
499
500// NewPlacementV1 creates a ServiceClient that may be used with the placement package.
501func NewPlacementV1(client *gophercloud.ProviderClient, eo gophercloud.EndpointOpts) (*gophercloud.ServiceClient, error) {
502	return initClientOpts(client, eo, "placement")
503}
504