Commit ea7289eb authored by Saverio Proto's avatar Saverio Proto

Add support to authenticate with application credential

parent 50b061b8
......@@ -84,6 +84,12 @@ type AuthOptions struct {
// Scope determines the scoping of the authentication request.
Scope *AuthScope `json:"-"`
// Authentication through Application Credentials requires supplying name, project and secret
// For project we can use TenantID
ApplicationCredentialID string `json:"-"`
ApplicationCredentialName string `json:"-"`
ApplicationCredentialSecret string `json:"-"`
}
// AuthScope allows a created token to be limited to a specific domain or project.
......@@ -142,7 +148,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
type userReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
Password string `json:"password"`
Password string `json:"password,omitempty"`
Domain *domainReq `json:"domain,omitempty"`
}
......@@ -154,10 +160,18 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
ID string `json:"id"`
}
type applicationCredentialReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,omitempty"`
User *userReq `json:"user,omitempty"`
Secret *string `json:"secret,omitempty"`
}
type identityReq struct {
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
Methods []string `json:"methods"`
Password *passwordReq `json:"password,omitempty"`
Token *tokenReq `json:"token,omitempty"`
ApplicationCredential *applicationCredentialReq `json:"application_credential,omitempty"`
}
type authReq struct {
......@@ -171,6 +185,7 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
// Populate the request structure based on the provided arguments. Create and return an error
// if insufficient or incompatible information is present.
var req request
var userRequest userReq
if opts.Password == "" {
if opts.TokenID != "" {
......@@ -194,8 +209,49 @@ func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[s
req.Auth.Identity.Token = &tokenReq{
ID: opts.TokenID,
}
} else if opts.ApplicationCredentialID != "" {
// Configure the request for ApplicationCredentialID authentication.
// https://github.com/openstack/keystoneauth/blob/stable/rocky/keystoneauth1/identity/v3/application_credential.py#L48-L67
// There are three kinds of possible application_credential requests
// 1. application_credential id + secret
// 2. application_credential name + secret + user_id
// 3. application_credential name + secret + username + domain_id / domain_name
if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
ID: &opts.ApplicationCredentialID,
Secret: &opts.ApplicationCredentialSecret,
}
} else if opts.ApplicationCredentialName != "" {
if opts.ApplicationCredentialSecret == "" {
return nil, ErrAppCredMissingSecret{}
}
// make sure that only one of DomainName or DomainID were provided
if opts.DomainID == "" && opts.DomainName == "" {
return nil, ErrDomainIDOrDomainName{}
}
req.Auth.Identity.Methods = []string{"application_credential"}
if opts.DomainID != "" {
userRequest = userReq{
Name: &opts.Username,
Domain: &domainReq{ID: &opts.DomainID},
}
} else if opts.DomainName != "" {
userRequest = userReq{
Name: &opts.Username,
Domain: &domainReq{Name: &opts.DomainName},
}
}
req.Auth.Identity.ApplicationCredential = &applicationCredentialReq{
Name: &opts.ApplicationCredentialName,
User: &userRequest,
Secret: &opts.ApplicationCredentialSecret,
}
} else {
// If no password or token ID are available, authentication can't continue.
// If no password or token ID or ApplicationCredential are available, authentication can't continue.
return nil, ErrMissingPassword{}
}
} else {
......
......@@ -451,3 +451,10 @@ type ErrScopeEmpty struct{ BaseError }
func (e ErrScopeEmpty) Error() string {
return "You must provide either a Project or Domain in a Scope"
}
// ErrAppCredMissingSecret indicates that no Application Credential Secret was provided with Application Credential ID or Name
type ErrAppCredMissingSecret struct{ BaseError }
func (e ErrAppCredMissingSecret) Error() string {
return "You must provide an Application Credential Secret"
}
......@@ -38,6 +38,9 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
tenantName := os.Getenv("OS_TENANT_NAME")
domainID := os.Getenv("OS_DOMAIN_ID")
domainName := os.Getenv("OS_DOMAIN_NAME")
applicationCredentialID := os.Getenv("OS_APPLICATION_CREDENTIAL_ID")
applicationCredentialName := os.Getenv("OS_APPLICATION_CREDENTIAL_NAME")
applicationCredentialSecret := os.Getenv("OS_APPLICATION_CREDENTIAL_SECRET")
// If OS_PROJECT_ID is set, overwrite tenantID with the value.
if v := os.Getenv("OS_PROJECT_ID"); v != "" {
......@@ -63,22 +66,32 @@ func AuthOptionsFromEnv() (gophercloud.AuthOptions, error) {
return nilOptions, err
}
if password == "" {
if password == "" && applicationCredentialID == "" && applicationCredentialName == "" {
err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_PASSWORD",
}
return nilOptions, err
}
if (applicationCredentialID != "" || applicationCredentialName != "") && applicationCredentialSecret == "" {
err := gophercloud.ErrMissingEnvironmentVariable{
EnvironmentVariable: "OS_APPLICATION_CREDENTIAL_SECRET",
}
return nilOptions, err
}
ao := gophercloud.AuthOptions{
IdentityEndpoint: authURL,
UserID: userID,
Username: username,
Password: password,
TenantID: tenantID,
TenantName: tenantName,
DomainID: domainID,
DomainName: domainName,
IdentityEndpoint: authURL,
UserID: userID,
Username: username,
Password: password,
TenantID: tenantID,
TenantName: tenantName,
DomainID: domainID,
DomainName: domainName,
ApplicationCredentialID: applicationCredentialID,
ApplicationCredentialName: applicationCredentialName,
ApplicationCredentialSecret: applicationCredentialSecret,
}
return ao, nil
......
......@@ -52,19 +52,28 @@ type AuthOptions struct {
// authentication token ID.
TokenID string `json:"-"`
// Authentication through Application Credentials requires supplying name, project and secret
// For project we can use TenantID
ApplicationCredentialID string `json:"-"`
ApplicationCredentialName string `json:"-"`
ApplicationCredentialSecret string `json:"-"`
Scope Scope `json:"-"`
}
// ToTokenV3CreateMap builds a request body from AuthOptions.
func (opts *AuthOptions) ToTokenV3CreateMap(scope map[string]interface{}) (map[string]interface{}, error) {
gophercloudAuthOpts := gophercloud.AuthOptions{
Username: opts.Username,
UserID: opts.UserID,
Password: opts.Password,
DomainID: opts.DomainID,
DomainName: opts.DomainName,
AllowReauth: opts.AllowReauth,
TokenID: opts.TokenID,
Username: opts.Username,
UserID: opts.UserID,
Password: opts.Password,
DomainID: opts.DomainID,
DomainName: opts.DomainName,
AllowReauth: opts.AllowReauth,
TokenID: opts.TokenID,
ApplicationCredentialID: opts.ApplicationCredentialID,
ApplicationCredentialName: opts.ApplicationCredentialName,
ApplicationCredentialSecret: opts.ApplicationCredentialSecret,
}
return gophercloudAuthOpts.ToTokenV3CreateMap(scope)
......
......@@ -275,6 +275,48 @@ func TestCreateProjectNameAndDomainNameScope(t *testing.T) {
`)
}
func TestCreateApplicationCredentialIDAndSecret(t *testing.T) {
authTokenPost(t, tokens.AuthOptions{ApplicationCredentialID: "12345abcdef", ApplicationCredentialSecret: "mysecret"}, nil, `
{
"auth": {
"identity": {
"application_credential": {
"id": "12345abcdef",
"secret": "mysecret"
},
"methods": [
"application_credential"
]
}
}
}
`)
}
func TestCreateApplicationCredentialNameAndSecret(t *testing.T) {
authTokenPost(t, tokens.AuthOptions{ApplicationCredentialName: "myappcred", ApplicationCredentialSecret: "mysecret", Username: "fenris", DomainName: "evil-plans"}, nil, `
{
"auth": {
"identity": {
"application_credential": {
"name": "myappcred",
"secret": "mysecret",
"user": {
"name": "fenris",
"domain": {
"name": "evil-plans"
}
}
},
"methods": [
"application_credential"
]
}
}
}
`)
}
func TestCreateExtractsTokenFromResponse(t *testing.T) {
testhelper.SetupHTTP()
defer testhelper.TeardownHTTP()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment