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