github.com/vnpaycloud-console/gophercloud/v2@v2.0.5/auth_options.go (about)

     1  package gophercloud
     2  
     3  /*
     4  AuthOptions stores information needed to authenticate to an OpenStack Cloud.
     5  You can populate one manually, or use a provider's AuthOptionsFromEnv() function
     6  to read relevant information from the standard environment variables. Pass one
     7  to a provider's AuthenticatedClient function to authenticate and obtain a
     8  ProviderClient representing an active session on that provider.
     9  
    10  Its fields are the union of those recognized by each identity implementation and
    11  provider.
    12  
    13  An 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(context.TODO(), opts)
    23  
    24  An example of using AuthOptionsFromEnv(), where the environment variables can
    25  be read from a file, such as a standard openrc file:
    26  
    27  	opts, err := openstack.AuthOptionsFromEnv()
    28  	provider, err := openstack.AuthenticatedClient(context.TODO(), opts)
    29  */
    30  type 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.
    99  type AuthScope struct {
   100  	ProjectID   string
   101  	ProjectName string
   102  	DomainID    string
   103  	DomainName  string
   104  	System      bool
   105  	TrustID     string
   106  }
   107  
   108  // ToTokenV2CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
   109  // interface in the v2 tokens package
   110  func (opts AuthOptions) ToTokenV2CreateMap() (map[string]any, error) {
   111  	// Populate the request map.
   112  	authMap := make(map[string]any)
   113  
   114  	if opts.Username != "" {
   115  		if opts.Password != "" {
   116  			authMap["passwordCredentials"] = map[string]any{
   117  				"username": opts.Username,
   118  				"password": opts.Password,
   119  			}
   120  		} else {
   121  			return nil, ErrMissingInput{Argument: "Password"}
   122  		}
   123  	} else if opts.TokenID != "" {
   124  		authMap["token"] = map[string]any{
   125  			"id": opts.TokenID,
   126  		}
   127  	} else {
   128  		return nil, ErrMissingInput{Argument: "Username"}
   129  	}
   130  
   131  	if opts.TenantID != "" {
   132  		authMap["tenantId"] = opts.TenantID
   133  	}
   134  	if opts.TenantName != "" {
   135  		authMap["tenantName"] = opts.TenantName
   136  	}
   137  
   138  	return map[string]any{"auth": authMap}, nil
   139  }
   140  
   141  // ToTokenV3CreateMap allows AuthOptions to satisfy the AuthOptionsBuilder
   142  // interface in the v3 tokens package
   143  func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]any) (map[string]any, error) {
   144  	type domainReq struct {
   145  		ID   *string `json:"id,omitempty"`
   146  		Name *string `json:"name,omitempty"`
   147  	}
   148  
   149  	type userReq struct {
   150  		ID       *string    `json:"id,omitempty"`
   151  		Name     *string    `json:"name,omitempty"`
   152  		Password *string    `json:"password,omitempty"`
   153  		Passcode *string    `json:"passcode,omitempty"`
   154  		Domain   *domainReq `json:"domain,omitempty"`
   155  	}
   156  
   157  	type passwordReq struct {
   158  		User userReq `json:"user"`
   159  	}
   160  
   161  	type tokenReq struct {
   162  		ID string `json:"id"`
   163  	}
   164  
   165  	type applicationCredentialReq struct {
   166  		ID     *string  `json:"id,omitempty"`
   167  		Name   *string  `json:"name,omitempty"`
   168  		User   *userReq `json:"user,omitempty"`
   169  		Secret *string  `json:"secret,omitempty"`
   170  	}
   171  
   172  	type totpReq struct {
   173  		User *userReq `json:"user,omitempty"`
   174  	}
   175  
   176  	type identityReq struct {
   177  		Methods               []string                  `json:"methods"`
   178  		Password              *passwordReq              `json:"password,omitempty"`
   179  		Token                 *tokenReq                 `json:"token,omitempty"`
   180  		ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
   181  		TOTP                  *totpReq                  `json:"totp,omitempty"`
   182  	}
   183  
   184  	type authReq struct {
   185  		Identity identityReq `json:"identity"`
   186  	}
   187  
   188  	type request struct {
   189  		Auth authReq `json:"auth"`
   190  	}
   191  
   192  	// Populate the request structure based on the provided arguments. Create and return an error
   193  	// if insufficient or incompatible information is present.
   194  	var req request
   195  
   196  	if opts.Password == "" && opts.Passcode == "" {
   197  		if opts.TokenID != "" {
   198  			// Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
   199  			// parameters.
   200  			if opts.Username != "" {
   201  				return nil, ErrUsernameWithToken{}
   202  			}
   203  			if opts.UserID != "" {
   204  				return nil, ErrUserIDWithToken{}
   205  			}
   206  			if opts.DomainID != "" {
   207  				return nil, ErrDomainIDWithToken{}
   208  			}
   209  			if opts.DomainName != "" {
   210  				return nil, ErrDomainNameWithToken{}
   211  			}
   212  
   213  			// Configure the request for Token authentication.
   214  			req.Auth.Identity.Methods = []string{"token"}
   215  			req.Auth.Identity.Token = &tokenReq{
   216  				ID: opts.TokenID,
   217  			}
   218  
   219  		} else if opts.ApplicationCredentialID != "" {
   220  			// Configure the request for ApplicationCredentialID authentication.
   221  			// https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67
   222  			// There are three kinds of possible application_credential requests
   223  			// 1. application_credential id + secret
   224  			// 2. application_credential name + secret + user_id
   225  			// 3. application_credential name + secret + username + domain_id / domain_name
   226  			if opts.ApplicationCredentialSecret == "" {
   227  				return nil, ErrAppCredMissingSecret{}
   228  			}
   229  			req.Auth.Identity.Methods = []string{"application_credential"}
   230  			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
   231  				ID:     &opts.ApplicationCredentialID,
   232  				Secret: &opts.ApplicationCredentialSecret,
   233  			}
   234  		} else if opts.ApplicationCredentialName != "" {
   235  			if opts.ApplicationCredentialSecret == "" {
   236  				return nil, ErrAppCredMissingSecret{}
   237  			}
   238  
   239  			var userRequest *userReq
   240  
   241  			if opts.UserID != "" {
   242  				// UserID could be used without the domain information
   243  				userRequest = &userReq{
   244  					ID: &opts.UserID,
   245  				}
   246  			}
   247  
   248  			if userRequest == nil && opts.Username == "" {
   249  				// Make sure that Username or UserID are provided
   250  				return nil, ErrUsernameOrUserID{}
   251  			}
   252  
   253  			if userRequest == nil && opts.DomainID != "" {
   254  				userRequest = &userReq{
   255  					Name:   &opts.Username,
   256  					Domain: &domainReq{ID: &opts.DomainID},
   257  				}
   258  			}
   259  
   260  			if userRequest == nil && opts.DomainName != "" {
   261  				userRequest = &userReq{
   262  					Name:   &opts.Username,
   263  					Domain: &domainReq{Name: &opts.DomainName},
   264  				}
   265  			}
   266  
   267  			// Make sure that DomainID or DomainName are provided among Username
   268  			if userRequest == nil {
   269  				return nil, ErrDomainIDOrDomainName{}
   270  			}
   271  
   272  			req.Auth.Identity.Methods = []string{"application_credential"}
   273  			req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
   274  				Name:   &opts.ApplicationCredentialName,
   275  				User:   userRequest,
   276  				Secret: &opts.ApplicationCredentialSecret,
   277  			}
   278  		} else {
   279  			// If no password or token ID or ApplicationCredential are available, authentication can't continue.
   280  			return nil, ErrMissingPassword{}
   281  		}
   282  	} else {
   283  		// Password authentication.
   284  		if opts.Password != "" {
   285  			req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "password")
   286  		}
   287  
   288  		// TOTP authentication.
   289  		if opts.Passcode != "" {
   290  			req.Auth.Identity.Methods = append(req.Auth.Identity.Methods, "totp")
   291  		}
   292  
   293  		// At least one of Username and UserID must be specified.
   294  		if opts.Username == "" && opts.UserID == "" {
   295  			return nil, ErrUsernameOrUserID{}
   296  		}
   297  
   298  		if opts.Username != "" {
   299  			// If Username is provided, UserID may not be provided.
   300  			if opts.UserID != "" {
   301  				return nil, ErrUsernameOrUserID{}
   302  			}
   303  
   304  			// Either DomainID or DomainName must also be specified.
   305  			if opts.DomainID == "" && opts.DomainName == "" {
   306  				return nil, ErrDomainIDOrDomainName{}
   307  			}
   308  
   309  			if opts.DomainID != "" {
   310  				if opts.DomainName != "" {
   311  					return nil, ErrDomainIDOrDomainName{}
   312  				}
   313  
   314  				// Configure the request for Username and Password authentication with a DomainID.
   315  				if opts.Password != "" {
   316  					req.Auth.Identity.Password = &passwordReq{
   317  						User: userReq{
   318  							Name:     &opts.Username,
   319  							Password: &opts.Password,
   320  							Domain:   &domainReq{ID: &opts.DomainID},
   321  						},
   322  					}
   323  				}
   324  				if opts.Passcode != "" {
   325  					req.Auth.Identity.TOTP = &totpReq{
   326  						User: &userReq{
   327  							Name:     &opts.Username,
   328  							Passcode: &opts.Passcode,
   329  							Domain:   &domainReq{ID: &opts.DomainID},
   330  						},
   331  					}
   332  				}
   333  			}
   334  
   335  			if opts.DomainName != "" {
   336  				// Configure the request for Username and Password authentication with a DomainName.
   337  				if opts.Password != "" {
   338  					req.Auth.Identity.Password = &passwordReq{
   339  						User: userReq{
   340  							Name:     &opts.Username,
   341  							Password: &opts.Password,
   342  							Domain:   &domainReq{Name: &opts.DomainName},
   343  						},
   344  					}
   345  				}
   346  
   347  				if opts.Passcode != "" {
   348  					req.Auth.Identity.TOTP = &totpReq{
   349  						User: &userReq{
   350  							Name:     &opts.Username,
   351  							Passcode: &opts.Passcode,
   352  							Domain:   &domainReq{Name: &opts.DomainName},
   353  						},
   354  					}
   355  				}
   356  			}
   357  		}
   358  
   359  		if opts.UserID != "" {
   360  			// If UserID is specified, neither DomainID nor DomainName may be.
   361  			if opts.DomainID != "" {
   362  				return nil, ErrDomainIDWithUserID{}
   363  			}
   364  			if opts.DomainName != "" {
   365  				return nil, ErrDomainNameWithUserID{}
   366  			}
   367  
   368  			// Configure the request for UserID and Password authentication.
   369  			if opts.Password != "" {
   370  				req.Auth.Identity.Password = &passwordReq{
   371  					User: userReq{
   372  						ID:       &opts.UserID,
   373  						Password: &opts.Password,
   374  					},
   375  				}
   376  			}
   377  
   378  			if opts.Passcode != "" {
   379  				req.Auth.Identity.TOTP = &totpReq{
   380  					User: &userReq{
   381  						ID:       &opts.UserID,
   382  						Passcode: &opts.Passcode,
   383  					},
   384  				}
   385  			}
   386  		}
   387  	}
   388  
   389  	b, err := BuildRequestBody(req, "")
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  
   394  	if len(scope) != 0 {
   395  		b["auth"].(map[string]any)["scope"] = scope
   396  	}
   397  
   398  	return b, nil
   399  }
   400  
   401  // ToTokenV3ScopeMap builds a scope from AuthOptions and satisfies interface in
   402  // the v3 tokens package.
   403  func (opts *AuthOptions) ToTokenV3ScopeMap() (map[string]any, error) {
   404  	// For backwards compatibility.
   405  	// If AuthOptions.Scope was not set, try to determine it.
   406  	// This works well for common scenarios.
   407  	if opts.Scope == nil {
   408  		opts.Scope = new(AuthScope)
   409  		if opts.TenantID != "" {
   410  			opts.Scope.ProjectID = opts.TenantID
   411  		} else {
   412  			if opts.TenantName != "" {
   413  				opts.Scope.ProjectName = opts.TenantName
   414  				opts.Scope.DomainID = opts.DomainID
   415  				opts.Scope.DomainName = opts.DomainName
   416  			}
   417  		}
   418  	}
   419  
   420  	if opts.Scope.System {
   421  		return map[string]any{
   422  			"system": map[string]any{
   423  				"all": true,
   424  			},
   425  		}, nil
   426  	}
   427  
   428  	if opts.Scope.TrustID != "" {
   429  		return map[string]any{
   430  			"OS-TRUST:trust": map[string]string{
   431  				"id": opts.Scope.TrustID,
   432  			},
   433  		}, nil
   434  	}
   435  
   436  	if opts.Scope.ProjectName != "" {
   437  		// ProjectName provided: either DomainID or DomainName must also be supplied.
   438  		// ProjectID may not be supplied.
   439  		if opts.Scope.DomainID == "" && opts.Scope.DomainName == "" {
   440  			return nil, ErrScopeDomainIDOrDomainName{}
   441  		}
   442  		if opts.Scope.ProjectID != "" {
   443  			return nil, ErrScopeProjectIDOrProjectName{}
   444  		}
   445  
   446  		if opts.Scope.DomainID != "" {
   447  			// ProjectName + DomainID
   448  			return map[string]any{
   449  				"project": map[string]any{
   450  					"name":   &opts.Scope.ProjectName,
   451  					"domain": map[string]any{"id": &opts.Scope.DomainID},
   452  				},
   453  			}, nil
   454  		}
   455  
   456  		if opts.Scope.DomainName != "" {
   457  			// ProjectName + DomainName
   458  			return map[string]any{
   459  				"project": map[string]any{
   460  					"name":   &opts.Scope.ProjectName,
   461  					"domain": map[string]any{"name": &opts.Scope.DomainName},
   462  				},
   463  			}, nil
   464  		}
   465  	} else if opts.Scope.ProjectID != "" {
   466  		// ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
   467  		if opts.Scope.DomainID != "" {
   468  			return nil, ErrScopeProjectIDAlone{}
   469  		}
   470  		if opts.Scope.DomainName != "" {
   471  			return nil, ErrScopeProjectIDAlone{}
   472  		}
   473  
   474  		// ProjectID
   475  		return map[string]any{
   476  			"project": map[string]any{
   477  				"id": &opts.Scope.ProjectID,
   478  			},
   479  		}, nil
   480  	} else if opts.Scope.DomainID != "" {
   481  		// DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
   482  		if opts.Scope.DomainName != "" {
   483  			return nil, ErrScopeDomainIDOrDomainName{}
   484  		}
   485  
   486  		// DomainID
   487  		return map[string]any{
   488  			"domain": map[string]any{
   489  				"id": &opts.Scope.DomainID,
   490  			},
   491  		}, nil
   492  	} else if opts.Scope.DomainName != "" {
   493  		// DomainName
   494  		return map[string]any{
   495  			"domain": map[string]any{
   496  				"name": &opts.Scope.DomainName,
   497  			},
   498  		}, nil
   499  	}
   500  
   501  	return nil, nil
   502  }
   503  
   504  func (opts AuthOptions) CanReauth() bool {
   505  	if opts.Passcode != "" {
   506  		// cannot reauth using TOTP passcode
   507  		return false
   508  	}
   509  
   510  	return opts.AllowReauth
   511  }
   512  
   513  // ToTokenV3HeadersMap allows AuthOptions to satisfy the AuthOptionsBuilder
   514  // interface in the v3 tokens package.
   515  func (opts *AuthOptions) ToTokenV3HeadersMap(map[string]any) (map[string]string, error) {
   516  	return nil, nil
   517  }