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	// At most one of DomainID and DomainName must be provided if using Username
49	// with Identity V3. Otherwise, either are optional.
50	DomainID   string `json:"-"`
51	DomainName string `json:"name,omitempty"`
52
53	// The TenantID and TenantName fields are optional for the Identity V2 API.
54	// The same fields are known as project_id and project_name in the Identity
55	// V3 API, but are collected as TenantID and TenantName here in both cases.
56	// Some providers allow you to specify a TenantName instead of the TenantId.
57	// Some require both. Your provider's authentication policies will determine
58	// how these fields influence authentication.
59	// If DomainID or DomainName are provided, they will also apply to TenantName.
60	// It is not currently possible to authenticate with Username and a Domain
61	// and scope to a Project in a different Domain by using TenantName. To
62	// accomplish that, the ProjectID will need to be provided as the TenantID
63	// option.
64	TenantID   string `json:"tenantId,omitempty"`
65	TenantName string `json:"tenantName,omitempty"`
66
67	// AllowReauth should be set to true if you grant permission for Gophercloud to
68	// cache your credentials in memory, and to allow Gophercloud to attempt to
69	// re-authenticate automatically if/when your token expires.  If you set it to
70	// false, it will not cache these settings, but re-authentication will not be
71	// possible.  This setting defaults to false.
72	//
73	// NOTE: The reauth function will try to re-authenticate endlessly if left
74	// unchecked. The way to limit the number of attempts is to provide a custom
75	// HTTP client to the provider client and provide a transport that implements
76	// the RoundTripper interface and stores the number of failed retries. For an
77	// example of this, see here:
78	// https://github.com/rackspace/rack/blob/1.0.0/auth/clients.go#L311
79	AllowReauth bool `json:"-"`
80
81	// TokenID allows users to authenticate (possibly as another user) with an
82	// authentication token ID.
83	TokenID string `json:"-"`
84
85	// Scope determines the scoping of the authentication request.
86	Scope *AuthScope `json:"-"`
87
88	// Authentication through Application Credentials requires supplying name, project and secret
89	// For project we can use TenantID
90	ApplicationCredentialID     string `json:"-"`
91	ApplicationCredentialName   string `json:"-"`
92	ApplicationCredentialSecret string `json:"-"`
93}
94
95// AuthScope allows a created token to be limited to a specific domain or project.
96type AuthScope struct {
97	ProjectID   string
98	ProjectName string
99	DomainID    string
100	DomainName  string
101}
102
103// ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
104// interface in the v2 tokens package
105func (opts AuthOptions) ToTokenV2CreateMap() (map[string]interface{}, error) {
106	// Populate the request map.
107	authMap := make(map[string]interface{})
108
109	if opts.Username != "" {
110		if opts.Password != "" {
111			authMap["passwordCredentials"] = map[string]interface{}{
112				"username": opts.Username,
113				"password": opts.Password,
114			}
115		} else {
116			return nil, ErrMissingInput{Argument: "Password"}
117		}
118	} else if opts.TokenID != "" {
119		authMap["token"] = map[string]interface{}{
120			"id": opts.TokenID,
121		}
122	} else {
123		return nil, ErrMissingInput{Argument: "Username"}
124	}
125
126	if opts.TenantID != "" {
127		authMap["tenantId"] = opts.TenantID
128	}
129	if opts.TenantName != "" {
130		authMap["tenantName"] = opts.TenantName
131	}
132
133	return map[string]interface{}{"auth": authMap}, nil
134}
135
136func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
137	type domainReq struct {
138		ID   *string `json:"id,omitempty"`
139		Name *string `json:"name,omitempty"`
140	}
141
142	type projectReq struct {
143		Domain *domainReq `json:"domain,omitempty"`
144		Name   *string    `json:"name,omitempty"`
145		ID     *string    `json:"id,omitempty"`
146	}
147
148	type userReq struct {
149		ID       *string    `json:"id,omitempty"`
150		Name     *string    `json:"name,omitempty"`
151		Password string     `json:"password,omitempty"`
152		Domain   *domainReq `json:"domain,omitempty"`
153	}
154
155	type passwordReq struct {
156		User userReq `json:"user"`
157	}
158
159	type tokenReq struct {
160		ID string `json:"id"`
161	}
162
163	type applicationCredentialReq struct {
164		ID     *string  `json:"id,omitempty"`
165		Name   *string  `json:"name,omitempty"`
166		User   *userReq `json:"user,omitempty"`
167		Secret *string  `json:"secret,omitempty"`
168	}
169
170	type identityReq struct {
171		Methods               []string                  `json:"methods"`
172		Password              *passwordReq              `json:"password,omitempty"`
173		Token                 *tokenReq                 `json:"token,omitempty"`
174		ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
175	}
176
177	type authReq struct {
178		Identity identityReq `json:"identity"`
179	}
180
181	type request struct {
182		Auth authReq `json:"auth"`
183	}
184
185	// Populate the request structure based on the provided arguments. Create and return an error
186	// if insufficient or incompatible information is present.
187	var req request
188
189	if opts.Password == "" {
190		if opts.TokenID != "" {
191			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
192			// parameters.
193			if opts.Username != "" {
194				return nil, ErrUsernameWithToken{}
195			}
196			if opts.UserID != "" {
197				return nil, ErrUserIDWithToken{}
198			}
199			if opts.DomainID != "" {
200				return nil, ErrDomainIDWithToken{}
201			}
202			if opts.DomainName != "" {
203				return nil, ErrDomainNameWithToken{}
204			}
205
206			// Configure the request for Token authentication.
207			req.Auth.Identity.Methods = []string{"token"}
208			req.Auth.Identity.Token = &tokenReq{
209				ID: opts.TokenID,
210			}
211
212		} else if opts.ApplicationCredentialID != "" {
213			// Configure the request for ApplicationCredentialID authentication.
214			// https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67
215			// There are three kinds of possible application_credential requests
216			// 1. application_credential id + secret
217			// 2. application_credential name + secret + user_id
218			// 3. application_credential name + secret + username + domain_id / domain_name
219			if opts.ApplicationCredentialSecret == "" {
220				return nil, ErrAppCredMissingSecret{}
221			}
222			req.Auth.Identity.Methods = []string{"application_credential"}
223			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
224				ID:     &opts.ApplicationCredentialID,
225				Secret: &opts.ApplicationCredentialSecret,
226			}
227		} else if opts.ApplicationCredentialName != "" {
228			if opts.ApplicationCredentialSecret == "" {
229				return nil, ErrAppCredMissingSecret{}
230			}
231
232			var userRequest *userReq
233
234			if opts.UserID != "" {
235				// UserID could be used without the domain information
236				userRequest = &userReq{
237					ID: &opts.UserID,
238				}
239			}
240
241			if userRequest == nil && opts.Username == "" {
242				// Make sure that Username or UserID are provided
243				return nil, ErrUsernameOrUserID{}
244			}
245
246			if userRequest == nil && opts.DomainID != "" {
247				userRequest = &userReq{
248					Name:   &opts.Username,
249					Domain: &domainReq{ID: &opts.DomainID},
250				}
251			}
252
253			if userRequest == nil && opts.DomainName != "" {
254				userRequest = &userReq{
255					Name:   &opts.Username,
256					Domain: &domainReq{Name: &opts.DomainName},
257				}
258			}
259
260			// Make sure that DomainID or DomainName are provided among Username
261			if userRequest == nil {
262				return nil, ErrDomainIDOrDomainName{}
263			}
264
265			req.Auth.Identity.Methods = []string{"application_credential"}
266			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
267				Name:   &opts.ApplicationCredentialName,
268				User:   userRequest,
269				Secret: &opts.ApplicationCredentialSecret,
270			}
271		} else {
272			// If no password or token ID or ApplicationCredential are available, authentication can't continue.
273			return nil, ErrMissingPassword{}
274		}
275	} else {
276		// Password authentication.
277		req.Auth.Identity.Methods = []string{"password"}
278
279		// At least one of Username and UserID must be specified.
280		if opts.Username == "" && opts.UserID == "" {
281			return nil, ErrUsernameOrUserID{}
282		}
283
284		if opts.Username != "" {
285			// If Username is provided, UserID may not be provided.
286			if opts.UserID != "" {
287				return nil, ErrUsernameOrUserID{}
288			}
289
290			// Either DomainID or DomainName must also be specified.
291			if opts.DomainID == "" && opts.DomainName == "" {
292				return nil, ErrDomainIDOrDomainName{}
293			}
294
295			if opts.DomainID != "" {
296				if opts.DomainName != "" {
297					return nil, ErrDomainIDOrDomainName{}
298				}
299
300				// Configure the request for Username and Password authentication with a DomainID.
301				req.Auth.Identity.Password = &passwordReq{
302					User: userReq{
303						Name:     &opts.Username,
304						Password: opts.Password,
305						Domain:   &domainReq{ID: &opts.DomainID},
306					},
307				}
308			}
309
310			if opts.DomainName != "" {
311				// Configure the request for Username and Password authentication with a DomainName.
312				req.Auth.Identity.Password = &passwordReq{
313					User: userReq{
314						Name:     &opts.Username,
315						Password: opts.Password,
316						Domain:   &domainReq{Name: &opts.DomainName},
317					},
318				}
319			}
320		}
321
322		if opts.UserID != "" {
323			// If UserID is specified, neither DomainID nor DomainName may be.
324			if opts.DomainID != "" {
325				return nil, ErrDomainIDWithUserID{}
326			}
327			if opts.DomainName != "" {
328				return nil, ErrDomainNameWithUserID{}
329			}
330
331			// Configure the request for UserID and Password authentication.
332			req.Auth.Identity.Password = &passwordReq{
333				User: userReq{ID: &opts.UserID, Password: opts.Password},
334			}
335		}
336	}
337
338	b, err := BuildRequestBody(req, "")
339	if err != nil {
340		return nil, err
341	}
342
343	if len(scope) != 0 {
344		b["auth"].(map[string]interface{})["scope"] = scope
345	}
346
347	return b, nil
348}
349
350func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]interface{}, error) {
351	// For backwards compatibility.
352	// If AuthOptions.Scope was not set, try to determine it.
353	// This works well for common scenarios.
354	if opts.Scope == nil {
355		opts.Scope = new(AuthScope)
356		if opts.TenantID != "" {
357			opts.Scope.ProjectID = opts.TenantID
358		} else {
359			if opts.TenantName != "" {
360				opts.Scope.ProjectName = opts.TenantName
361				opts.Scope.DomainID = opts.DomainID
362				opts.Scope.DomainName = opts.DomainName
363			}
364		}
365	}
366
367	if opts.Scope.ProjectName != "" {
368		// ProjectName provided: either DomainID or DomainName must also be supplied.
369		// ProjectID may not be supplied.
370		if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
371			return nil, ErrScopeDomainIDOrDomainName{}
372		}
373		if opts.Scope.ProjectID != "" {
374			return nil, ErrScopeProjectIDOrProjectName{}
375		}
376
377		if opts.Scope.DomainID != "" {
378			// ProjectName + DomainID
379			return map[string]interface{}{
380				"project": map[string]interface{}{
381					"name":   &opts.Scope.ProjectName,
382					"domain": map[string]interface{}{"id": &opts.Scope.DomainID},
383				},
384			}, nil
385		}
386
387		if opts.Scope.DomainName != "" {
388			// ProjectName + DomainName
389			return map[string]interface{}{
390				"project": map[string]interface{}{
391					"name":   &opts.Scope.ProjectName,
392					"domain": map[string]interface{}{"name": &opts.Scope.DomainName},
393				},
394			}, nil
395		}
396	} else if opts.Scope.ProjectID != "" {
397		// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
398		if opts.Scope.DomainID != "" {
399			return nil, ErrScopeProjectIDAlone{}
400		}
401		if opts.Scope.DomainName != "" {
402			return nil, ErrScopeProjectIDAlone{}
403		}
404
405		// ProjectID
406		return map[string]interface{}{
407			"project": map[string]interface{}{
408				"id": &opts.Scope.ProjectID,
409			},
410		}, nil
411	} else if opts.Scope.DomainID != "" {
412		// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
413		if opts.Scope.DomainName != "" {
414			return nil, ErrScopeDomainIDOrDomainName{}
415		}
416
417		// DomainID
418		return map[string]interface{}{
419			"domain": map[string]interface{}{
420				"id": &opts.Scope.DomainID,
421			},
422		}, nil
423	} else if opts.Scope.DomainName != "" {
424		// DomainName
425		return map[string]interface{}{
426			"domain": map[string]interface{}{
427				"name": &opts.Scope.DomainName,
428			},
429		}, nil
430	}
431
432	return nil, nil
433}
434
435func (opts AuthOptions) CanReauth() bool {
436	return opts.AllowReauth
437}
438