1package gophercloud
2
3/*
4AuthOptions stores information needed to authenticate to an OpenStack Cloud.
5You can populate one manually, or use a provider's AuthOptionsFromEnv() function
6to read relevant information from the standard environment variables. Pass one
7to a provider's AuthenticatedClient function to authenticate and obtain a
8ProviderClient representing an active session on that provider.
9
10Its fields are the union of those recognized by each identity implementation and
11provider.
12
13An example of manually providing authentication information:
14
15  opts := gophercloud.AuthOptions{
16    IdentityEndpoint: "https://openstack.example.com:5000/v2.0",
17    Username: "{username}",
18    Password: "{password}",
19    TenantID: "{tenant_id}",
20  }
21
22  provider, err := openstack.AuthenticatedClient(opts)
23
24An example of using AuthOptionsFromEnv(), where the environment variables can
25be read from a file, such as a standard openrc file:
26
27  opts, err := openstack.AuthOptionsFromEnv()
28  provider, err := openstack.AuthenticatedClient(opts)
29*/
30type AuthOptions struct {
31	// IdentityEndpoint specifies the HTTP endpoint that is required to work with
32	// the Identity API of the appropriate version. While it's ultimately needed by
33	// all of the identity services, it will often be populated by a provider-level
34	// function.
35	//
36	// The IdentityEndpoint is typically referred to as the "auth_url" or
37	// "OS_AUTH_URL" in the information provided by the cloud operator.
38	IdentityEndpoint string `json:"-"`
39
40	// Username is required if using Identity V2 API. Consult with your provider's
41	// control panel to discover your account's username. In Identity V3, either
42	// UserID or a combination of Username and DomainID or DomainName are needed.
43	Username string `json:"username,omitempty"`
44	UserID   string `json:"-"`
45
46	Password string `json:"password,omitempty"`
47
48	// Passcode is used in TOTP authentication method
49	Passcode string `json:"passcode,omitempty"`
50
51	// At most one of DomainID and DomainName must be provided if using Username
52	// with Identity V3. Otherwise, either are optional.
53	DomainID   string `json:"-"`
54	DomainName string `json:"name,omitempty"`
55
56	// The TenantID and TenantName fields are optional for the Identity V2 API.
57	// The same fields are known as project_id and project_name in the Identity
58	// V3 API, but are collected as TenantID and TenantName here in both cases.
59	// Some providers allow you to specify a TenantName instead of the TenantId.
60	// Some require both. Your provider's authentication policies will determine
61	// how these fields influence authentication.
62	// If DomainID or DomainName are provided, they will also apply to TenantName.
63	// It is not currently possible to authenticate with Username and a Domain
64	// and scope to a Project in a different Domain by using TenantName. To
65	// accomplish that, the ProjectID will need to be provided as the TenantID
66	// option.
67	TenantID   string `json:"tenantId,omitempty"`
68	TenantName string `json:"tenantName,omitempty"`
69
70	// AllowReauth should be set to true if you grant permission for Gophercloud to
71	// cache your credentials in memory, and to allow Gophercloud to attempt to
72	// re-authenticate automatically if/when your token expires.  If you set it to
73	// false, it will not cache these settings, but re-authentication will not be
74	// possible.  This setting defaults to false.
75	//
76	// NOTE: The reauth function will try to re-authenticate endlessly if left
77	// unchecked. The way to limit the number of attempts is to provide a custom
78	// HTTP client to the provider client and provide a transport that implements
79	// the RoundTripper interface and stores the number of failed retries. For an
80	// example of this, see here:
81	// https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
82	AllowReauth bool `json:"-"`
83
84	// TokenID allows users to authenticate (possibly as another user) with an
85	// authentication token ID.
86	TokenID string `json:"-"`
87
88	// Scope determines the scoping of the authentication request.
89	Scope *AuthScope `json:"-"`
90
91	// Authentication through Application Credentials requires supplying name, project and secret
92	// For project we can use TenantID
93	ApplicationCredentialID     string `json:"-"`
94	ApplicationCredentialName   string `json:"-"`
95	ApplicationCredentialSecret string `json:"-"`
96}
97
98// AuthScope allows a created token to be limited to a specific domain or project.
99type AuthScope struct {
100	ProjectID   string
101	ProjectName string
102	DomainID    string
103	DomainName  string
104	System      bool
105}
106
107// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
108// interface in the v2 tokens package
109func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
110	// Populate the request map.
111	authMap := make(map[string]interface{})
112
113	if opts.Username != "" {
114		if opts.Password != "" {
115			authMap["passwordCredentials"] = map[string]interface{}{
116				"username": opts.Username,
117				"password": opts.Password,
118			}
119		} else {
120			return nil, ErrMissingInput{Argument: "Password"}
121		}
122	} else if opts.TokenID != "" {
123		authMap["token"] = map[string]interface{}{
124			"id": opts.TokenID,
125		}
126	} else {
127		return nil, ErrMissingInput{Argument: "Username"}
128	}
129
130	if opts.TenantID != "" {
131		authMap["tenantId"] = opts.TenantID
132	}
133	if opts.TenantName != "" {
134		authMap["tenantName"] = opts.TenantName
135	}
136
137	return map[string]interface{}{"auth": authMap}, nil
138}
139
140func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
141	type domainReq struct {
142		ID   *string `json:"id,omitempty"`
143		Name *string `json:"name,omitempty"`
144	}
145
146	type projectReq struct {
147		Domain *domainReq `json:"domain,omitempty"`
148		Name   *string    `json:"name,omitempty"`
149		ID     *string    `json:"id,omitempty"`
150	}
151
152	type userReq struct {
153		ID       *string    `json:"id,omitempty"`
154		Name     *string    `json:"name,omitempty"`
155		Password *string    `json:"password,omitempty"`
156		Passcode *string    `json:"passcode,omitempty"`
157		Domain   *domainReq `json:"domain,omitempty"`
158	}
159
160	type passwordReq struct {
161		User userReq `json:"user"`
162	}
163
164	type tokenReq struct {
165		ID string `json:"id"`
166	}
167
168	type applicationCredentialReq struct {
169		ID     *string  `json:"id,omitempty"`
170		Name   *string  `json:"name,omitempty"`
171		User   *userReq `json:"user,omitempty"`
172		Secret *string  `json:"secret,omitempty"`
173	}
174
175	type totpReq struct {
176		User *userReq `json:"user,omitempty"`
177	}
178
179	type identityReq struct {
180		Methods               []string                  `json:"methods"`
181		Password              *passwordReq              `json:"password,omitempty"`
182		Token                 *tokenReq                 `json:"token,omitempty"`
183		ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
184		TOTP                  *totpReq                  `json:"totp,omitempty"`
185	}
186
187	type authReq struct {
188		Identity identityReq `json:"identity"`
189	}
190
191	type request struct {
192		Auth authReq `json:"auth"`
193	}
194
195	// Populate the request structure based on the provided arguments. Create and return an error
196	// if insufficient or incompatible information is present.
197	var req request
198
199	if opts.Password == "" && opts.Passcode == "" {
200		if opts.TokenID != "" {
201			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
202			// parameters.
203			if opts.Username != "" {
204				return nil, ErrUsernameWithToken{}
205			}
206			if opts.UserID != "" {
207				return nil, ErrUserIDWithToken{}
208			}
209			if opts.DomainID != "" {
210				return nil, ErrDomainIDWithToken{}
211			}
212			if opts.DomainName != "" {
213				return nil, ErrDomainNameWithToken{}
214			}
215
216			// Configure the request for Token authentication.
217			req.Auth.Identity.Methods = []string{"token"}
218			req.Auth.Identity.Token = &tokenReq{
219				ID: opts.TokenID,
220			}
221
222		} else if opts.ApplicationCredentialID != "" {
223			// Configure the request for ApplicationCredentialID authentication.
224			// https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67
225			// There are three kinds of possible application_credential requests
226			// 1. application_credential id + secret
227			// 2. application_credential name + secret + user_id
228			// 3. application_credential name + secret + username + domain_id / domain_name
229			if opts.ApplicationCredentialSecret == "" {
230				return nil, ErrAppCredMissingSecret{}
231			}
232			req.Auth.Identity.Methods = []string{"application_credential"}
233			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
234				ID:     &opts.ApplicationCredentialID,
235				Secret: &opts.ApplicationCredentialSecret,
236			}
237		} else if opts.ApplicationCredentialName != "" {
238			if opts.ApplicationCredentialSecret == "" {
239				return nil, ErrAppCredMissingSecret{}
240			}
241
242			var userRequest *userReq
243
244			if opts.UserID != "" {
245				// UserID could be used without the domain information
246				userRequest = &userReq{
247					ID: &opts.UserID,
248				}
249			}
250
251			if userRequest == nil && opts.Username == "" {
252				// Make sure that Username or UserID are provided
253				return nil, ErrUsernameOrUserID{}
254			}
255
256			if userRequest == nil && opts.DomainID != "" {
257				userRequest = &userReq{
258					Name:   &opts.Username,
259					Domain: &domainReq{ID: &opts.DomainID},
260				}
261			}
262
263			if userRequest == nil && opts.DomainName != "" {
264				userRequest = &userReq{
265					Name:   &opts.Username,
266					Domain: &domainReq{Name: &opts.DomainName},
267				}
268			}
269
270			// Make sure that DomainID or DomainName are provided among Username
271			if userRequest == nil {
272				return nil, ErrDomainIDOrDomainName{}
273			}
274
275			req.Auth.Identity.Methods = []string{"application_credential"}
276			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
277				Name:   &opts.ApplicationCredentialName,
278				User:   userRequest,
279				Secret: &opts.ApplicationCredentialSecret,
280			}
281		} else {
282			// If no password or token ID or ApplicationCredential are available, authentication can't continue.
283			return nil, ErrMissingPassword{}
284		}
285	} else {
286		// Password authentication.
287		if opts.Password != "" {
288			req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password")
289		}
290
291		// TOTP authentication.
292		if opts.Passcode != "" {
293			req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "totp")
294		}
295
296		// At least one of Username and UserID must be specified.
297		if opts.Username == "" && opts.UserID == "" {
298			return nil, ErrUsernameOrUserID{}
299		}
300
301		if opts.Username != "" {
302			// If Username is provided, UserID may not be provided.
303			if opts.UserID != "" {
304				return nil, ErrUsernameOrUserID{}
305			}
306
307			// Either DomainID or DomainName must also be specified.
308			if opts.DomainID == "" && opts.DomainName == "" {
309				return nil, ErrDomainIDOrDomainName{}
310			}
311
312			if opts.DomainID != "" {
313				if opts.DomainName != "" {
314					return nil, ErrDomainIDOrDomainName{}
315				}
316
317				// Configure the request for Username and Password authentication with a DomainID.
318				if opts.Password != "" {
319					req.Auth.Identity.Password = &passwordReq{
320						User: userReq{
321							Name:     &opts.Username,
322							Password: &opts.Password,
323							Domain:   &domainReq{ID: &opts.DomainID},
324						},
325					}
326				}
327				if opts.Passcode != "" {
328					req.Auth.Identity.TOTP = &totpReq{
329						User: &userReq{
330							Name:     &opts.Username,
331							Passcode: &opts.Passcode,
332							Domain:   &domainReq{ID: &opts.DomainID},
333						},
334					}
335				}
336			}
337
338			if opts.DomainName != "" {
339				// Configure the request for Username and Password authentication with a DomainName.
340				if opts.Password != "" {
341					req.Auth.Identity.Password = &passwordReq{
342						User: userReq{
343							Name:     &opts.Username,
344							Password: &opts.Password,
345							Domain:   &domainReq{Name: &opts.DomainName},
346						},
347					}
348				}
349
350				if opts.Passcode != "" {
351					req.Auth.Identity.TOTP = &totpReq{
352						User: &userReq{
353							Name:     &opts.Username,
354							Passcode: &opts.Passcode,
355							Domain:   &domainReq{Name: &opts.DomainName},
356						},
357					}
358				}
359			}
360		}
361
362		if opts.UserID != "" {
363			// If UserID is specified, neither DomainID nor DomainName may be.
364			if opts.DomainID != "" {
365				return nil, ErrDomainIDWithUserID{}
366			}
367			if opts.DomainName != "" {
368				return nil, ErrDomainNameWithUserID{}
369			}
370
371			// Configure the request for UserID and Password authentication.
372			if opts.Password != "" {
373				req.Auth.Identity.Password = &passwordReq{
374					User: userReq{
375						ID:       &opts.UserID,
376						Password: &opts.Password,
377					},
378				}
379			}
380
381			if opts.Passcode != "" {
382				req.Auth.Identity.TOTP = &totpReq{
383					User: &userReq{
384						ID:       &opts.UserID,
385						Passcode: &opts.Passcode,
386					},
387				}
388			}
389		}
390	}
391
392	b, err := BuildRequestBody(req, "")
393	if err != nil {
394		return nil, err
395	}
396
397	if len(scope) != 0 {
398		b["auth"].(map[string]interface{})["scope"] = scope
399	}
400
401	return b, nil
402}
403
404func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
405	// For backwards compatibility.
406	// If AuthOptions.Scope was not set, try to determine it.
407	// This works well for common scenarios.
408	if opts.Scope == nil {
409		opts.Scope = new(AuthScope)
410		if opts.TenantID != "" {
411			opts.Scope.ProjectID = opts.TenantID
412		} else {
413			if opts.TenantName != "" {
414				opts.Scope.ProjectName = opts.TenantName
415				opts.Scope.DomainID = opts.DomainID
416				opts.Scope.DomainName = opts.DomainName
417			}
418		}
419	}
420
421	if opts.Scope.System {
422		return map[string]interface{}{
423			"system": map[string]interface{}{
424				"all": true,
425			},
426		}, nil
427	}
428
429	if opts.Scope.ProjectName != "" {
430		// ProjectName provided: either DomainID or DomainName must also be supplied.
431		// ProjectID may not be supplied.
432		if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
433			return nil, ErrScopeDomainIDOrDomainName{}
434		}
435		if opts.Scope.ProjectID != "" {
436			return nil, ErrScopeProjectIDOrProjectName{}
437		}
438
439		if opts.Scope.DomainID != "" {
440			// ProjectName + DomainID
441			return map[string]interface{}{
442				"project": map[string]interface{}{
443					"name":   &opts.Scope.ProjectName,
444					"domain": map[string]interface{}{"id": &opts.Scope.DomainID},
445				},
446			}, nil
447		}
448
449		if opts.Scope.DomainName != "" {
450			// ProjectName + DomainName
451			return map[string]interface{}{
452				"project": map[string]interface{}{
453					"name":   &opts.Scope.ProjectName,
454					"domain": map[string]interface{}{"name": &opts.Scope.DomainName},
455				},
456			}, nil
457		}
458	} else if opts.Scope.ProjectID != "" {
459		// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
460		if opts.Scope.DomainID != "" {
461			return nil, ErrScopeProjectIDAlone{}
462		}
463		if opts.Scope.DomainName != "" {
464			return nil, ErrScopeProjectIDAlone{}
465		}
466
467		// ProjectID
468		return map[string]interface{}{
469			"project": map[string]interface{}{
470				"id": &opts.Scope.ProjectID,
471			},
472		}, nil
473	} else if opts.Scope.DomainID != "" {
474		// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
475		if opts.Scope.DomainName != "" {
476			return nil, ErrScopeDomainIDOrDomainName{}
477		}
478
479		// DomainID
480		return map[string]interface{}{
481			"domain": map[string]interface{}{
482				"id": &opts.Scope.DomainID,
483			},
484		}, nil
485	} else if opts.Scope.DomainName != "" {
486		// DomainName
487		return map[string]interface{}{
488			"domain": map[string]interface{}{
489				"name": &opts.Scope.DomainName,
490			},
491		}, nil
492	}
493
494	return nil, nil
495}
496
497func (opts AuthOptions) CanReauth() bool {
498	if opts.Passcode != "" {
499		// cannot reauth using TOTP passcode
500		return false
501	}
502
503	return opts.AllowReauth
504}
505