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