Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[maia] Added application credential authentication #61

Merged
merged 39 commits into from
Oct 23, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
348934b
fix URL scoping /w basic auth
Sep 6, 2019
cdf7d20
[maia] Added application credential authentication
Sep 12, 2019
78219d1
[maia] Fixed typo
Sep 12, 2019
06dd09d
[maia] Adjust logic for linting
Sep 12, 2019
0321ed2
[maia] Added tests and fixed logic
Sep 13, 2019
ba70dbb
[maia] Added expansion value for application cred secret
Sep 18, 2019
7963c1c
[maia] Added comment for token cache
Sep 18, 2019
4f2f271
[maia] Added documentation for app cred authorization
Sep 18, 2019
9e51432
[maia] Camel Cased app cred vars
Sep 18, 2019
760ef95
[maia] Adjusted checks for failed authentication
Sep 18, 2019
ccd3cf8
digging deeper into the asServiceUser case
Sep 19, 2019
6740ef8
Merge branch 'master' of https://github.com/sapcc/maia
Oct 8, 2019
fc84ad3
Improve documentation (#58)
Oct 9, 2019
c3cb746
essential Thanos support (#62)
Oct 9, 2019
95b27ac
Accelerate label values (#60)
Oct 10, 2019
61a11a6
Merge branch 'master' of https://github.com/sapcc/maia
Oct 14, 2019
7e53e41
add test for prometheus driver
Oct 14, 2019
fcb496d
add test for prometheus driver (#63)
Oct 14, 2019
eefad48
Merge branch 'master' of https://github.com/sapcc/maia
Oct 14, 2019
98eb7ba
remove Golang 1.11 build
Oct 14, 2019
c61fe8e
improve docs (#64)
Oct 14, 2019
369b9d4
[maia] Added application credential authentication
Sep 12, 2019
73c219b
[maia] Fixed typo
Sep 12, 2019
4b0716a
[maia] Adjust logic for linting
Sep 12, 2019
7a5696b
[maia] Added tests and fixed logic
Sep 13, 2019
ff560a3
[maia] Added expansion value for application cred secret
Sep 18, 2019
91d936f
[maia] Added comment for token cache
Sep 18, 2019
c113499
[maia] Added documentation for app cred authorization
Sep 18, 2019
8122876
[maia] Camel Cased app cred vars
Sep 18, 2019
5142df3
[maia] Adjusted checks for failed authentication
Sep 18, 2019
80c4808
digging deeper into the asServiceUser case
Sep 19, 2019
131bb67
Merge branch 'enable-application-credentials' of https://github.com/s…
Oct 15, 2019
8a26aa8
fix build errors
Oct 15, 2019
a4706e6
remove asServiceUser parameter override
Oct 15, 2019
2a71ba8
adapt tests to app-creds
Oct 16, 2019
28338fe
add user documentation
Oct 16, 2019
ceaa479
refine
Oct 16, 2019
1f76010
add grafana doc
Oct 16, 2019
239847c
Update users-guide.md
Oct 16, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 24 additions & 5 deletions pkg/cmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,22 +88,38 @@ func fetchToken() {

if authType == "password" {
auth.TokenID = ""
auth.ApplicationCredentialName = ""
auth.ApplicationCredentialID = ""
auth.ApplicationCredentialSecret = ""
} else if authType == "token" {
auth.Password = ""
auth.UserID = ""
auth.Username = ""
auth.DomainID = ""
auth.DomainName = ""
auth.ApplicationCredentialName = ""
auth.ApplicationCredentialID = ""
auth.ApplicationCredentialSecret = ""
} else if authType == "application_credential" {
auth.Password = ""
auth.UserID = ""
auth.DomainID = ""
auth.DomainName = ""
auth.TokenID = ""
}

if auth.TokenID == "" {
if (auth.Username == "" && auth.UserID == "") || auth.Password == "" {
panic(fmt.Errorf("You must specify either --os-token or provide --os-username / --os-user-id and --os-password"))
if ((auth.Username == "" && auth.UserID == "") || auth.Password == "") &&
jobrs marked this conversation as resolved.
Show resolved Hide resolved
(auth.ApplicationCredentialID == "" || auth.ApplicationCredentialSecret == "") &&
(auth.ApplicationCredentialName == "" || auth.Username == "") {
panic(fmt.Errorf("You must specify either --os-token or provide --os-username / --os-user-id and --os-password " +
"or --os-application-credential-name and os-username or --os-application-credential-id and --os-application-credential-secret"))
}
}

if auth.TokenID != "" && auth.Password != "" {
panic(fmt.Errorf("--os-token and --os-password listed, can only use one. Setting --os-auth-type sets which authentication type to use"))
if (auth.TokenID != "" && auth.Password != "") || (auth.TokenID != "" && auth.ApplicationCredentialSecret != "") {
panic(fmt.Errorf("Multiple authentications specified (--os-password, --os-token, --os-application-credential-secret), " +
"can only use one. Setting --os-auth-type sets which authentication type to use"))
}
context, url, err := keystoneInstance().Authenticate(auth)
if err != nil {
Expand Down Expand Up @@ -643,7 +659,10 @@ func init() {
RootCmd.PersistentFlags().StringVar(&scopedDomain, "os-domain-name", os.Getenv("OS_DOMAIN_NAME"), "OpenStack domain name to scope to")
RootCmd.PersistentFlags().StringVar(&auth.Scope.DomainID, "os-domain-id", os.Getenv("OS_DOMAIN_ID"), "OpenStack domain ID to scope to")
RootCmd.PersistentFlags().StringVar(&auth.TokenID, "os-token", "$OS_TOKEN", "OpenStack keystone token") // avoid showing contents of $OS_TOKEN as default value
RootCmd.PersistentFlags().StringVar(&authType, "os-auth-type", os.Getenv("OS_AUTH_TYPE"), "OpenStack authentication type ('password' or 'token')")
RootCmd.PersistentFlags().StringVar(&authType, "os-auth-type", os.Getenv("OS_AUTH_TYPE"), "OpenStack authentication type ('password' or 'token' or application_credential)")
RootCmd.PersistentFlags().StringVar(&auth.ApplicationCredentialName, "os-application-credential-name", os.Getenv("OS_APPLICATION_CREDENTIAL_NAME"), "OpenStack application credential name")
RootCmd.PersistentFlags().StringVar(&auth.ApplicationCredentialID, "os-application-credential-id", os.Getenv("OS_APPLICATION_CREDENTIAL_ID"), "OpenStack application credential id")
RootCmd.PersistentFlags().StringVar(&auth.ApplicationCredentialSecret, "os-application-credential-secret", "$OS_APPLICATION_CREDENTIAL_SECRET", "OpenStack application credential secret") // avoid showing contents of $OS_PASSWORD as default value
jobrs marked this conversation as resolved.
Show resolved Hide resolved

RootCmd.PersistentFlags().StringVarP(&outputFormat, "format", "f", "", "Specify output format: table, json, template or value")
RootCmd.PersistentFlags().StringVarP(&columns, "columns", "c", "", "Specify the columns to print (comma-separated; only when --format value is set)")
Expand Down
75 changes: 48 additions & 27 deletions pkg/cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,13 @@ func setupTest(controller *gomock.Controller) (*keystone.MockDriver, *storage.Mo

// set mandatory parameters
auth = gophercloud.AuthOptions{
IdentityEndpoint: "",
Username: "username",
UserID: "user_id",
Password: "testwd",
IdentityEndpoint: "",
Username: "username",
UserID: "user_id",
Password: "testwd",
ApplicationCredentialID: "appcredid",
ApplicationCredentialName: "appcredname",
ApplicationCredentialSecret: "appcredsecret",
Scope: &gophercloud.AuthScope{
ProjectID: "12345"}}

Expand All @@ -77,8 +80,10 @@ func setupTest(controller *gomock.Controller) (*keystone.MockDriver, *storage.Mo
}

func expectAuth(keystoneMock *keystone.MockDriver) {
keystoneMock.EXPECT().Authenticate(gophercloud.AuthOptions{IdentityEndpoint: auth.IdentityEndpoint, Username: auth.Username, UserID: auth.UserID, Password: auth.Password, DomainName: auth.DomainName, Scope: auth.Scope}).Return(&policy.Context{Request: map[string]string{"user_id": auth.UserID,
"project_id": auth.Scope.ProjectID, "password": auth.Password}, Auth: map[string]string{"project_id": auth.Scope.ProjectID}, Roles: []string{"monitoring_viewer"}}, "http://localhost:9091", nil)
keystoneMock.EXPECT().Authenticate(gophercloud.AuthOptions{IdentityEndpoint: auth.IdentityEndpoint, Username: auth.Username, UserID: auth.UserID, Password: auth.Password, ApplicationCredentialID: auth.ApplicationCredentialID,
ApplicationCredentialName: auth.ApplicationCredentialName, ApplicationCredentialSecret: auth.ApplicationCredentialSecret, DomainName: auth.DomainName, Scope: auth.Scope}).Return(&policy.Context{Request: map[string]string{"user_id": auth.UserID,
"project_id": auth.Scope.ProjectID, "password": auth.Password, "application_credential_id": auth.ApplicationCredentialID, "application_credentail_name": auth.ApplicationCredentialName, "application_credential_secret": auth.ApplicationCredentialSecret},
Auth: map[string]string{"project_id": auth.Scope.ProjectID}, Roles: []string{"monitoring_viewer"}}, "http://localhost:9091", nil)
// call this explicitly since the mocked storage does not
fetchToken()
}
Expand Down Expand Up @@ -412,23 +417,30 @@ func ExampleQuery_rangeSeriesTable() {

func Test_Auth(t *testing.T) {
tt := []struct {
name string
tokenid string
authtype string
username string
userid string
password string
expectpanic bool
name string
tokenid string
authtype string
username string
userid string
password string
appcredid string
appcredname string
appcredsecret string
expectpanic bool
}{
{"passwithauthtype", "", "password", "testname", "testid", "testwd", false},
{"passwithoutauthtype", "", "", "testname", "testid", "testwd", false},
{"tokenwithpasswithauthtype", "ABC", "token", "testname", "testid", "testwd", false},
{"tokenwithpasswithoutauthtype", "ABC", "", "testname", "testid", "testwd", true},
{"passwithauthtype", "", "password", "testname", "testid", "testwd", "", "", "", false},
{"passwithoutauthtype", "", "", "testname", "testid", "testwd", "", "", "", false},
{"tokenwithpasswithauthtype", "ABC", "token", "testname", "testid", "testwd", "", "", "", false},
{"tokenwithpasswithoutauthtype", "ABC", "", "testname", "testid", "testwd", "", "", "", true},
{"appcredidwithsecret", "", "application_credential", "", "", "", "testappcredid", "", "testappcredsecret", false},
{"appcrednamewithusername", "", "application_credential", "testname", "", "", "", "testappcredname", "", false},
{"appcrednamewithoutusername", "", "application_credential", "", "", "", "", "testappcredname", "", true},
{"appcredidwithoutsecret", "", "application_credential", "testname", "", "", "testappcredid", "", "", true},
}

for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
paniced := authentication(tc.tokenid, tc.authtype, tc.username, tc.userid, tc.password)
paniced := authentication(tc.tokenid, tc.authtype, tc.username, tc.userid, tc.password, tc.appcredid, tc.appcredname, tc.appcredsecret)
if paniced != tc.expectpanic {
t.Errorf("Panic does not match desired result for test: %v", tc)
}
Expand All @@ -437,7 +449,7 @@ func Test_Auth(t *testing.T) {

}

func authentication(tokenid, authtype, username, userid, password string) (paniced bool) {
func authentication(tokenid, authtype, username, userid, password, appcredid, appcredname, appcredsecret string) (paniced bool) {
paniced = false

defer func() {
Expand Down Expand Up @@ -465,11 +477,14 @@ func authentication(tokenid, authtype, username, userid, password string) (panic

// set mandatory parameters
auth = gophercloud.AuthOptions{
IdentityEndpoint: "",
Username: username,
UserID: userid,
Password: password,
TokenID: tokenid,
IdentityEndpoint: "",
Username: username,
UserID: userid,
Password: password,
ApplicationCredentialID: appcredid,
ApplicationCredentialName: appcredname,
ApplicationCredentialSecret: appcredsecret,
TokenID: tokenid,
Scope: &gophercloud.AuthScope{
ProjectID: "12345"}}
expectedAuth := auth
Expand All @@ -480,16 +495,22 @@ func authentication(tokenid, authtype, username, userid, password string) (panic
expectedAuth.Password = ""
expectedAuth.UserID = ""
expectedAuth.Username = ""
expectedAuth.ApplicationCredentialID = ""
expectedAuth.ApplicationCredentialName = ""
expectedAuth.ApplicationCredentialSecret = ""
}

// create dummy keystone and storage mock
keystoneMock := keystone.NewMockDriver(ctrl)
setKeystoneInstance(keystoneMock)
keystoneMock.EXPECT().Authenticate(expectedAuth).Return(&policy.Context{
Request: map[string]string{
"user_id": auth.UserID,
"project_id": auth.Scope.ProjectID,
"password": auth.Password},
"user_id": auth.UserID,
"project_id": auth.Scope.ProjectID,
"password": auth.Password,
"application_credential_id": auth.ApplicationCredentialID,
"application_credential_name": auth.ApplicationCredentialName,
"application_credential_secret": auth.ApplicationCredentialSecret},
Auth: map[string]string{"project_id": auth.Scope.ProjectID},
Roles: []string{"monitoring_viewer"},
}, "http://localhost:9091", nil)
Expand Down
78 changes: 59 additions & 19 deletions pkg/keystone/keystone.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ type keystoneToken struct {
ProjectScope keystoneTokenThingInDomain `json:"project"`
Roles []keystoneTokenThing `json:"roles"`
User keystoneTokenThingInDomain `json:"user"`
Application keystoneTokenThingInDomain `json:"application"`
Token string
ExpiresAt string `json:"expires_at"`
}
Expand All @@ -147,23 +148,27 @@ func (t *keystoneToken) ToContext() policy.Context {
c := policy.Context{
Roles: make([]string, 0, len(t.Roles)),
Auth: map[string]string{
"user_id": t.User.ID,
"user_name": t.User.Name,
"user_domain_id": t.User.Domain.ID,
"user_domain_name": t.User.Domain.Name,
"domain_id": t.DomainScope.ID,
"domain_name": t.DomainScope.Name,
"project_id": t.ProjectScope.ID,
"project_name": t.ProjectScope.Name,
"project_domain_id": t.ProjectScope.Domain.ID,
"project_domain_name": t.ProjectScope.Domain.Name,
"token": t.Token,
"token-expiry": t.ExpiresAt,
"user_id": t.User.ID,
"user_name": t.User.Name,
"user_domain_id": t.User.Domain.ID,
"user_domain_name": t.User.Domain.Name,
"application_credential_id": t.Application.ID,
"application_credential_name": t.Application.Name,
"domain_id": t.DomainScope.ID,
"domain_name": t.DomainScope.Name,
"project_id": t.ProjectScope.ID,
"project_name": t.ProjectScope.Name,
"project_domain_id": t.ProjectScope.Domain.ID,
"project_domain_name": t.ProjectScope.Domain.Name,
"token": t.Token,
"token-expiry": t.ExpiresAt,
},
Request: map[string]string{
"user_id": t.User.ID,
"domain_id": t.DomainScope.ID,
"project_id": t.ProjectScope.ID,
"user_id": t.User.ID,
"domain_id": t.DomainScope.ID,
"project_id": t.ProjectScope.ID,
"application_credential_id": t.Application.ID,
"application_credential_name": t.Application.Name,
},
Logger: util.LogDebug,
}
Expand Down Expand Up @@ -264,6 +269,12 @@ func authOpts2StringKey(authOpts gophercloud.AuthOptions) string {

// build unique key by separating fields with blanks. Since blanks are not allowed in several of those
// the result will be unique
if authOpts.ApplicationCredentialID != "" || authOpts.ApplicationCredentialName != "" {
return authOpts.UserID + " " + authOpts.Username + " " + authOpts.Password + " " + authOpts.DomainID + " " +
authOpts.DomainName + " " + authOpts.ApplicationCredentialID + " " + authOpts.ApplicationCredentialName + " " +
authOpts.ApplicationCredentialSecret
jobrs marked this conversation as resolved.
Show resolved Hide resolved
}

return authOpts.UserID + " " + authOpts.Username + " " + authOpts.Password + " " + authOpts.DomainID + " " +
authOpts.DomainName + " " + authOpts.Scope.ProjectID + " " + authOpts.Scope.ProjectName + " " +
authOpts.Scope.DomainID + " " + authOpts.Scope.DomainName
Expand Down Expand Up @@ -300,6 +311,10 @@ func (d *keystone) AuthenticateRequest(r *http.Request, guessScope bool) (*polic
r.Header.Set("X-User-Name", context.Auth["user_name"])
r.Header.Set("X-User-Domain-Id", context.Auth["user_domain_id"])
r.Header.Set("X-User-Domain-Name", context.Auth["user_domain_name"])
r.Header.Set("X-Application-Credential-Id", context.Auth["application_credential_id"])
jobrs marked this conversation as resolved.
Show resolved Hide resolved
r.Header.Set("X-Application-Credential-Name", context.Auth["application_credential_name"])
r.Header.Set("X-Application-Credential-Secret", context.Auth["application_credential_secret"])

if context.Auth["project_id"] != "" {
r.Header.Set("X-Project-Id", context.Auth["project_id"])
r.Header.Set("X-Project-Name", context.Auth["project_name"])
Expand Down Expand Up @@ -330,6 +345,12 @@ func (d *keystone) authOptionsFromRequest(r *http.Request, guessScope bool) (*go
AllowReauth: false,
}

// Get application credentials from header
jobrs marked this conversation as resolved.
Show resolved Hide resolved
appcredid := r.Header.Get("X-Application-Credential-Id")
appcredsecret := r.Header.Get("X-Application-Credential-secret")
appcredname := r.Header.Get("X-Application-Credential-Name")
appcredusername := r.Header.Get("X-User-Name")

// extract credentials
query := r.URL.Query()
if token := r.Header.Get("X-Auth-Token"); token != "" {
Expand Down Expand Up @@ -384,6 +405,13 @@ func (d *keystone) authOptionsFromRequest(r *http.Request, guessScope bool) (*go

// set password
ba.Password = password
// if application credentials are used, skip th basic auth checks below
} else if (appcredid != "" && appcredsecret != "") ||
(appcredname != "" && appcredusername != "") {
ba.ApplicationCredentialID = appcredid
ba.ApplicationCredentialName = appcredname
jobrs marked this conversation as resolved.
Show resolved Hide resolved
ba.ApplicationCredentialSecret = appcredsecret
return &ba, nil
} else {
return nil, NewAuthenticationError(StatusMissingCredentials, "Authorization header missing (no username/password or token)")
}
Expand Down Expand Up @@ -433,6 +461,9 @@ func (d *keystone) guessScope(ba *gophercloud.AuthOptions) AuthenticationError {
// It returns the authorization context
func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser bool, rescope bool) (*policy.Context, string, AuthenticationError) {
// check cache, which does not work if tokens are rescoped
if authOpts.ApplicationCredentialName != "" || authOpts.ApplicationCredentialID != "" {
jobrs marked this conversation as resolved.
Show resolved Hide resolved
asServiceUser = false
}
if entry, found := d.tokenCache.Get(authOpts2StringKey(authOpts)); found && (authOpts.Scope == nil || authOpts.Scope.ProjectID == entry.(*cacheEntry).context.Auth["project_id"]) {
if authOpts.TokenID != "" {
util.LogDebug("Token cache hit: token %s... for scope %+v", authOpts.TokenID[:1+len(authOpts.TokenID)/4], authOpts.Scope)
Expand All @@ -441,6 +472,7 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser
}
return entry.(*cacheEntry).context, entry.(*cacheEntry).endpointURL, nil
}

//use a custom token struct instead of tokens.Token which is way incomplete
var tokenData keystoneToken
var endpointURL string
Expand Down Expand Up @@ -488,6 +520,8 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser
util.LogInfo("Failed login of user name %s%s for scope %+v: %s", authOpts.Username, authOpts.UserID, authOpts.Scope, err.Error())
} else if authOpts.TokenID != "" {
util.LogInfo("Failed login of with token %s... for scope %+v: %s", authOpts.TokenID[:1+len(authOpts.TokenID)/4], authOpts.Scope, err.Error())
} else if authOpts.ApplicationCredentialID != "" || authOpts.ApplicationCredentialSecret != "" {
jobrs marked this conversation as resolved.
Show resolved Hide resolved
util.LogInfo("Failed login of application credentials %s%s: %s", authOpts.ApplicationCredentialID, authOpts.ApplicationCredentialName, err.Error())
} else {
statusCode = StatusMissingCredentials
}
Expand All @@ -513,10 +547,16 @@ func (d *keystone) authenticate(authOpts gophercloud.AuthOptions, asServiceUser
tokenData.User.Name = authOpts.Username
tokenData.User.Domain.ID = authOpts.DomainID
tokenData.User.Domain.Name = authOpts.DomainName
tokenData.ProjectScope.ID = authOpts.Scope.ProjectID
tokenData.ProjectScope.Name = authOpts.Scope.ProjectName
tokenData.DomainScope.ID = authOpts.Scope.DomainID
tokenData.ProjectScope.Name = authOpts.Scope.DomainName
if authOpts.Scope != nil {
tokenData.ProjectScope.ID = authOpts.Scope.ProjectID
tokenData.ProjectScope.Name = authOpts.Scope.ProjectName
tokenData.DomainScope.ID = authOpts.Scope.DomainID
tokenData.ProjectScope.Name = authOpts.Scope.DomainName
}
if authOpts.ApplicationCredentialName != "" || authOpts.ApplicationCredentialID != "" {
tokenData.Application.ID = authOpts.ApplicationCredentialID
tokenData.Application.Name = authOpts.ApplicationCredentialName
}

endpointURL, err = client.EndpointLocator(metricsEndpointOpts)
if err != nil {
Expand Down