8000 Appcred auth support by simonepelosi · Pull Request #109 · go-goose/goose · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ func newClient(creds *identity.Credentials, auth_method identity.AuthMode, httpC
client_creds.URL = client_creds.URL[:len(client_creds.URL)-1]
}
switch auth_method {
case identity.AuthUserPassV3:
case identity.AuthUserPassV3, identity.AuthApplicationCredentialV3:
client_creds.URL = client_creds.URL + apiTokensV3
default:
client_creds.URL = client_creds.URL + apiTokens
Expand Down
72 changes: 55 additions & 17 deletions identity/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ import (
type AuthMode int

const (
AuthLegacy = AuthMode(iota) // Legacy authentication
AuthUserPass // Username + password authentication
AuthKeyPair // Access/secret key pair authentication
AuthUserPassV3 // Username + password authentication (v3 API)
AuthLegacy = AuthMode(iota) // Legacy authentication
AuthUserPass // Username + password authentication
AuthKeyPair // Access/secret key pair authentication
AuthUserPassV3 // Username + password authentication (v3 API)
AuthApplicationCredentialV3 // Application credential authentication (v3 API)
)

func (a AuthMode) String() string {
Expand All @@ -35,6 +36,8 @@ func (a AuthMode) String() string {
return "Username/password Authentication"
case AuthUserPassV3:
return "Username/password Authentication (Version 3)"
case AuthApplicationCredentialV3:
return "Application Credential Authentication (Version 3)"
}
panic(fmt.Errorf("Unknown athentication type: %d", a))
}
Expand Down Expand Up @@ -62,17 +65,19 @@ type AuthDetails struct {
// Credentials defines necessary parameters for authentication.
// TODO - Tenant is deprecated, migrate attribute names to Project.
type Credentials struct {
URL string // The URL to authenticate against
User string // The username to authenticate as
Secrets string // The secrets to pass
Region string // Region to send requests to
TenantName string `credentials:"optional"` // The project name for this connection
TenantID string `credentials:"optional"` // The project ID for this connection
Version int `credentials:"optional"` // The Keystone version
Domain string `credentials:"optional"` // The domain for authorization (new in keystone v3)
UserDomain string `credentials:"optional"` // The owning domain for this user (new in keystone v3)
ProjectDomain string `credentials:"optional"` // The project domain for authorization (new in keystone v3)
TrustID string `credentials:"optional"` // The trust ID for authorization (new in keystone v3)
URL string // The URL to authenticate against
User string // The username to authenticate as
Secrets string // The secrets to pass
Region 10BC8 string // Region to send requests to
TenantName string `credentials:"optional"` // The project name for this connection
TenantID string `credentials:"optional"` // The project ID for this connection
Version int `credentials:"optional"` // The Keystone version
Domain string `credentials:"optional"` // The domain for authorization (new in keystone v3)
UserDomain string `credentials:"optional"` // The owning domain for this user (new in keystone v3)
ProjectDomain string `credentials:"optional"` // The project domain for authorization (new in keystone v3)
TrustID string `credentials:"optional"` // The trust ID for authorization (new in keystone v3)
ApplicationCredentialID string `credentials:"optional"` // The application credential ID (new in keystone v3)
ApplicationCredentialSecret string `credentials:"optional"` // The application credential secret (new in keystone v3)
}

// Authenticator is implemented by each authentication method.
Expand Down Expand Up @@ -157,6 +162,16 @@ var (
CredEnvTrustID = []string{
"OS_TRUST_ID",
}
// CredEnvApplicationCredentialID is used for
// Credentials.ApplicationCredentialID.
CredEnvApplicationCredentialID = []string{
"OS_APPLICATION_CREDENTIAL_ID",
}
// CredEnvApplicationCredentialSecret is used for
// Credentials.ApplicationCredentialSecret.
CredEnvApplicationCredentialSecret = []string{
"OS_APPLICATION_CREDENTIAL_SECRET",
}
)

// CredentialsFromEnv creates and initializes the credentials from the
Expand All @@ -173,6 +188,12 @@ func CredentialsFromEnv() (*Credentials, error) {
UserDomain: getConfig(CredEnvUserDomainName),
ProjectDomain: getConfig(CredEnvProjectDomainName),
TrustID: getConfig(CredEnvTrustID),
ApplicationCredentialID: getConfig(
CredEnvApplicationCredentialID,
),
ApplicationCredentialSecret: getConfig(
CredEnvApplicationCredentialSecret,
),
}
defaultDomain := getConfig(CredEnvDefaultDomainName)
if defaultDomain != "" {
Expand All @@ -197,18 +218,33 @@ func CredentialsFromEnv() (*Credentials, error) {

// CompleteCredentialsFromEnv gets and verifies all the required
// authentication parameters have values in the environment.
//
// Username/password (User, Secrets) and application credential
// (ApplicationCredentialID, ApplicationCredentialSecret) are
// alternative credential sets: Keystone application credentials
// carry no username/password, so a caller authenticating that way
// is not expected to have OS_USERNAME/OS_PASSWORD set. If either
// application-credential field is present, User/Secrets are not
// required; otherwise the existing username/password check is
// unchanged.
func CompleteCredentialsFromEnv() (cred *Credentials, err error) {
cred, err = CredentialsFromEnv()
if err != nil {
return &Credentials{}, err
}
usingAppCred := cred.ApplicationCredentialID != "" ||
cred.ApplicationCredentialSecret != ""
v := reflect.ValueOf(cred).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
for i := range v.NumField() {
f := v.Field(i)
name := t.Field(i).Name
if usingAppCred && (name == "User" || name == "Secrets") {
continue
}
tag := t.Field(i).Tag.Get("credentials")
if f.String() == "" && tag != "optional" {
err = fmt.Errorf("required environment variable not set for credentials attribute: %s", t.Field(i).Name)
err = fmt.Errorf("required environment variable not set for credentials attribute: %s", name)
}
}
return
Expand All @@ -232,6 +268,8 @@ func NewAuthenticator(authMode AuthMode, httpClient goosehttp.HttpClient) Authen
return &KeyPair{client: httpClient}
case AuthUserPassV3:
return &V3UserPass{client: httpClient}
case AuthApplicationCredentialV3:
return &V3AppCred{client: httpClient}
}
}

Expand Down
25 changes: 25 additions & 0 deletions identity/identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,31 @@ func (s *CredentialsTestSuite) TestCompleteCredentialsFromEnvKeypairCompatibleEn
c.Check(creds.Domain, gc.Equals, "domain-name")
}

// An application credential (id+secret) is an alternative to
// username/password: a standard environment with only
// OS_AUTH_URL, OS_APPLICATION_CREDENTIAL_ID and
// OS_APPLICATION_CREDENTIAL_SECRET set (no OS_USERNAME/OS_PASSWORD,
// no access/secret key pair) must complete successfully.
func (s *CredentialsTestSuite) TestCompleteCredentialsFromEnvApplicationCredential(c *gc.C) {
env := map[string]string{
"OS_AUTH_URL": "http://auth",
"OS_REGION_NAME": "region",
"OS_APPLICATION_CREDENTIAL_ID": "cred-id",
"OS_APPLICATION_CREDENTIAL_SECRET": "cred-secret",
}
for key, value := range env {
os.Setenv(key, value)
}
creds, err := CompleteCredentialsFromEnv()
c.Assert(err, gc.IsNil)
c.Check(creds.URL, gc.Equals, "http://auth")
c.Check(creds.Region, gc.Equals, "region")
c.Check(creds.User, gc.Equals, "")
c.Check(creds.Secrets, gc.Equals, "")
c.Check(creds.ApplicationCredentialID, gc.Equals, "cred-id")
c.Check(creds.ApplicationCredentialSecret, gc.Equals, "cred-secret")
}

func (s *CredentialsTestSuite) TestCompleteCredentialsCheckProjectNameAliasVars(c *gc.C) {
env := map[string]string{
// required env vars
Expand Down
53 changes: 53 additions & 0 deletions identity/v3appcred.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package identity

import (
"fmt"

goosehttp "github.com/go-goose/goose/v5/http"
)

// v3AuthApplicationCredential contains an application credential
// authentication request, as described in:
// https://docs.openstack.org/api-ref/identity/v3/#application-credentials
type v3AuthApplicationCredential struct {
ID string `json:"id,omitempty"`
Secret string `json:"secret"`
}

// V3AppCred is an Authenticator that will perform application
// credential authentication using the v3 protocol.
type V3AppCred struct {
client goosehttp.HttpClient
}

// Auth performs a v3 application credential authentication request
// using the values supplied in creds.
//
// Unlike V3UserPass, this never sets an explicit scope on the
// request: an application credential is already scoped to a single
// project at creation time, and Keystone rejects an auth request
// that both uses an application credential and asks for an explicit
// project scope.
func (a *V3AppCred) Auth(creds *Credentials) (*AuthDetails, error) {
if a.client == nil {
a.client = goosehttp.New()
}
if creds.ApplicationCredentialID == "" {
return nil, fmt.Errorf("application credential id not specified")
}
if creds.ApplicationCredentialSecret == "" {
return nil, fmt.Errorf("application credential secret not specified")
}
auth := v3AuthWrapper{
Auth: v3AuthRequest{
Identity: v3AuthIdentity{
Methods: []string{"application_credential"},
ApplicationCredential: &v3AuthApplicationCredential{
ID: creds.ApplicationCredentialID,
Secret: creds.ApplicationCredentialSecret,
},
},
},
}
return v3KeystoneAuth(a.client, &auth, creds.URL)
}
149 changes: 149 additions & 0 deletions identity/v3appcred_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package identity

import (
"net/http"

gc "gopkg.in/check.v1"

"github.com/go-goose/goose/v5/testing/httpsuite"
"github.com/go-goose/goose/v5/testservices/identityservice"
)

type V3AppCredTestSuite struct {
httpsuite.HTTPSuite
}

var _ = gc.Suite(&V3AppCredTestSuite{})

func (s *V3AppCredTestSuite) TestAuthAgainstServer(c *gc.C) {
service := identityservice.NewV3AppCred()
service.SetupHTTP(s.Mux)
credInfo := service.AddCredential(
"cred-id", "cred-secret", "token-value",
"user-id", "project-id", "project-name",
)
var l Authenticator = &V3AppCred{}
creds := Credentials{
URL: s.Server.URL + "/v3/auth/tokens",
ApplicationCredentialID: "cred-id",
ApplicationCredentialSecret: "cred-secret",
}
auth, err := l.Auth(&creds)
c.Assert(err, gc.IsNil)
c.Assert(auth.Token, gc.Equals, credInfo.Token)
c.Assert(auth.TenantId, gc.Equals, credInfo.ProjectId)
c.Assert(auth.TenantName, gc.Equals, credInfo.ProjectName)
c.Assert(auth.UserId, gc.Equals, credInfo.UserId)
}

func (s *V3AppCredTestSuite) TestAuthWrongSecret(c *gc.C) {
service := identityservice.NewV3AppCred()
service.SetupHTTP(s.Mux)
service.AddCredential(
"cred-id", "cred-secret", "token-value",
"user-id", "project-id", "project-name",
)
var l Authenticator = &V3AppCred{}
creds := Credentials{
URL: s.Server.URL + "/v3/auth/tokens",
ApplicationCredentialID: "cred-id",
ApplicationCredentialSecret: "wrong-secret",
}
_, err := l.Auth(&creds)
c.Assert(err, gc.NotNil)
}

func (s *V3AppCredTestSuite) TestAuthMissingID(c *gc.C) {
var l Authenticator = &V3AppCred{}
creds := Credentials{
URL: "http://example.invalid/v3/auth/tokens",
ApplicationCredentialSecret: "cred-secret",
}
_, err := l.Auth(&creds)
c.Assert(err, gc.ErrorMatches, "application credential id not specified")
}

func (s *V3AppCredTestSuite) TestAuthMissingSecret(c *gc.C) {
var l Authenticator = &V3AppCred{}
creds := Credentials{
URL: "http://example.invalid/v3/auth/tokens",
ApplicationCredentialID: "cred-id",
}
_, err := l.Auth(&creds)
c.Assert(
err, gc.ErrorMatches, "application credential secret not specified",
)
}

// Even if a caller mistakenly sets TenantName/Domain on the
// Credentials (fields meant for password auth), V3AppCred must never
// put a scope on the wire: Keystone rejects a scoped request for an
// already project-scoped application credential.
func (s *V3AppCredTestSuite) TestAuthNeverSendsScope(c *gc.C) {
service := identityservice.NewV3AppCred()
service.SetupHTTP(s.Mux)
credInfo := service.AddCredential(
"cred-id", "cred-secret", "token-value",
"user-id", "project-id", "project-name",
)
var l Authenticator = &V3AppCred{}
creds := Credentials{
URL: s.Server.URL + "/v3/auth/tokens",
ApplicationCredentialID: "cred-id",
ApplicationCredentialSecret: "cred-secret",
TenantName: "some-other-project",
Domain: "some-domain",
}
auth, err := l.Auth(&creds)
c.Assert(err, gc.IsNil)
c.Assert(auth.Token, gc.Equals, credInfo.Token)
}

// Regression test: a Keystone deployment has been observed to omit
// the timezone offset on expires_at/issued_at specifically for
// application-credential-scoped tokens (e.g. "...123456", no "Z"),
// which time.Time's strict RFC3339 parsing rejects outright. The
// fake V3AppCred service always serialises these via time.Time
// (always RFC3339, always a "Z"), so it can never exercise this
// fallback; serve a raw, handwritten response here instead.
func (s *V3AppCredTestSuite) TestAuthNoTimezoneOnTimestamps(c *gc.C) {
s.Mux.HandleFunc(
"/v3/auth/tokens",
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Subject-Token", "token-value")
w.WriteHeader(http.StatusCreated)
_, _ = w.Write([]byte(`{
"token": {
"expires_at": "2030-01-01T00:00:00.123456",
"issued_at": "2020-01-01T00:00:00.123456",
"methods": ["application_credential"],
"catalog": [],
"project": {
"id": "project-id",
"name": "project-name",
"domain": {"id": "", "name": ""}
},
"domain": {"id": "", "name": ""},
"user": {
"id": "user-id",
"name": "",
"domain": {"id": "", "name": ""}
}
}
}`))
},
)
var l Authenticator = &V3AppCred{}
creds := Credentials{
URL: s.Server.URL + "/v3/auth/tokens",
ApplicationCredentialID: "cred-id",
ApplicationCredentialSecret: "cred-secret",
}
auth, err := l.Auth(&creds)
c.Assert(err, gc.IsNil)
c.Assert(auth.Token, gc.Equals, "token-value")
c.Assert(auth.TenantId, gc.Equals, "project-id")
c.Assert(auth.TenantName, gc.Equals, "project-name")
c.Assert(auth.UserId, gc.Equals, "user-id")
}
Loading
Loading
0