The first identity v3 request.
diff --git a/openstack/identity/v3/client.go b/openstack/identity/v3/client.go
new file mode 100644
index 0000000..164958f
--- /dev/null
+++ b/openstack/identity/v3/client.go
@@ -0,0 +1,24 @@
+package v3
+
+import "github.com/rackspace/gophercloud"
+
+// Client abstracts the connection information necessary to make API calls to Identity v3
+// resources.
+type Client struct {
+ gophercloud.ServiceClient
+
+ // TokenID is redudant storage for an active token.
+ // The Identity service occasionally needs to access the assigned token directly, but I don't want to export it from all
+ // service clients unless we absolutely need to.
+ TokenID string
+}
+
+var (
+ nilClient = Client{}
+)
+
+// NewClient attempts to authenticate to the v3 identity endpoint. Returns a populated
+// IdentityV3Client on success or an error on failure.
+func NewClient(authOptions gophercloud.AuthOptions) (*Client, error) {
+ return &nilClient, nil
+}
diff --git a/openstack/identity/v3/doc.go b/openstack/identity/v3/doc.go
new file mode 100644
index 0000000..8a9335a
--- /dev/null
+++ b/openstack/identity/v3/doc.go
@@ -0,0 +1,4 @@
+/*
+Package v3 implements v3 of the OpenStack identity API.
+*/
+package v3
diff --git a/openstack/identity/v3/tokens/requests.go b/openstack/identity/v3/tokens/requests.go
new file mode 100644
index 0000000..625f8d7
--- /dev/null
+++ b/openstack/identity/v3/tokens/requests.go
@@ -0,0 +1,317 @@
+package tokens
+
+import (
+ "errors"
+ "fmt"
+
+ "github.com/racker/perigee"
+ "github.com/rackspace/gophercloud"
+ identity "github.com/rackspace/gophercloud/openstack/identity/v3"
+)
+
+func unacceptedAttributeErr(attribute string) error {
+ return fmt.Errorf("The base Identity V3 API does not accept authentication by %s", attribute)
+}
+
+func redundantWithTokenErr(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a TokenID", attribute)
+}
+
+func redundantWithUserID(attribute string) error {
+ return fmt.Errorf("%s may not be provided when authenticating with a UserID", attribute)
+}
+
+var (
+ // ErrAPIKeyProvided indicates that an APIKey was provided but can't be used.
+ ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
+
+ // ErrTenantIDProvided indicates that a TenantID was provided but can't be used.
+ ErrTenantIDProvided = unacceptedAttributeErr("TenantID")
+
+ // ErrTenantNameProvided indicates that a TenantName was provided but can't be used.
+ ErrTenantNameProvided = unacceptedAttributeErr("TenantName")
+
+ // ErrUsernameWithToken indicates that a Username was provided, but token authentication is being used instead.
+ ErrUsernameWithToken = redundantWithTokenErr("Username")
+
+ // ErrUserIDWithToken indicates that a UserID was provided, but token authentication is being used instead.
+ ErrUserIDWithToken = redundantWithTokenErr("UserID")
+
+ // ErrDomainIDWithToken indicates that a DomainID was provided, but token authentication is being used instead.
+ ErrDomainIDWithToken = redundantWithTokenErr("DomainID")
+
+ // ErrDomainNameWithToken indicates that a DomainName was provided, but token authentication is being used instead.s
+ ErrDomainNameWithToken = redundantWithTokenErr("DomainName")
+
+ // ErrUsernameOrUserID indicates that neither username nor userID are specified, or both are at once.
+ ErrUsernameOrUserID = errors.New("Exactly one of Username and UserID must be provided for password authentication")
+
+ // ErrDomainIDWithUserID indicates that a DomainID was provided, but unnecessary because a UserID is being used.
+ ErrDomainIDWithUserID = redundantWithUserID("DomainID")
+
+ // ErrDomainNameWithUserID indicates that a DomainName was provided, but unnecessary because a UserID is being used.
+ ErrDomainNameWithUserID = redundantWithUserID("DomainName")
+
+ // ErrDomainIDOrDomainName indicates that a username was provided, but no domain to scope it.
+ // It may also indicate that both a DomainID and a DomainName were provided at once.
+ ErrDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName to authenticate by Username")
+
+ // ErrMissingPassword indicates that no password was provided and no token is available.
+ ErrMissingPassword = errors.New("You must provide a password to authenticate")
+
+ // ErrScopeDomainIDOrDomainName indicates that a domain ID or Name was required in a Scope, but not present.
+ ErrScopeDomainIDOrDomainName = errors.New("You must provide exactly one of DomainID or DomainName in a Scope with ProjectName")
+
+ // ErrScopeProjectIDOrProjectName indicates that both a ProjectID and a ProjectName were provided in a Scope.
+ ErrScopeProjectIDOrProjectName = errors.New("You must provide at most one of ProjectID or ProjectName in a Scope")
+
+ // ErrScopeProjectIDAlone indicates that a ProjectID was provided with other constraints in a Scope.
+ ErrScopeProjectIDAlone = errors.New("ProjectID must be supplied alone in a Scope")
+
+ // ErrScopeDomainName indicates that a DomainName was provided alone in a Scope.
+ ErrScopeDomainName = errors.New("DomainName must be supplied with a ProjectName or ProjectID in a Scope.")
+
+ // ErrScopeEmpty indicates that no credentials were provided in a Scope.
+ ErrScopeEmpty = errors.New("You must provide either a Project or Domain in a Scope")
+)
+
+// TokenCreateResult contains the document structure returned from a Create call.
+type TokenCreateResult map[string]interface{}
+
+// GetTokenID retrieves a token generated by a Create call from an token creation response.
+func (r TokenCreateResult) GetTokenID() (string, error) {
+ return "", nil
+}
+
+// Scope allows a created token to be limited to a specific domain or project.
+type Scope struct {
+ ProjectID string
+ ProjectName string
+ DomainID string
+ DomainName string
+}
+
+// Create authenticates and generates a new token.
+func Create(c *identity.Client, ao gophercloud.AuthOptions, scope *Scope) (gophercloud.AuthResults, error) {
+ type domainReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"id,omitempty"`
+ }
+
+ type projectReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Name *string `json:"name,omitempty"`
+ ID *string `json:"id,omitempty"`
+ }
+
+ type userReq struct {
+ ID *string `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Password string `json:"password"`
+ Domain *domainReq `json:"domain"`
+ }
+
+ type passwordReq struct {
+ User userReq `json:"user"`
+ }
+
+ type tokenReq struct {
+ ID string `json:"id"`
+ }
+
+ type identityReq struct {
+ Methods []string `json:"methods"`
+ Password *passwordReq `json:"token,omitempty"`
+ Token *tokenReq `json:"token,omitempty"`
+ }
+
+ type scopeReq struct {
+ Domain *domainReq `json:"domain,omitempty"`
+ Project *projectReq `json:"project,omitempty"`
+ }
+
+ type authReq struct {
+ Identity identityReq `json:"identity"`
+ Scope *scopeReq `json:"scope"`
+ }
+
+ type request struct {
+ Auth authReq `json:"auth"`
+ }
+
+ // Populate the request structure based on the provided arguments. Create and return an error
+ // if insufficient or incompatible information is present.
+ var req request
+
+ // Test first for unrecognized arguments.
+ if ao.APIKey != "" {
+ return nil, ErrAPIKeyProvided
+ }
+ if ao.TenantID != "" {
+ return nil, ErrTenantIDProvided
+ }
+ if ao.TenantName != "" {
+ return nil, ErrTenantNameProvided
+ }
+
+ if ao.Password == "" {
+ if c.TokenID != "" {
+ // Because we aren't using password authentication, it's an error to also provide any of the user-based authentication
+ // parameters.
+ if ao.Username != "" {
+ return nil, ErrUsernameWithToken
+ }
+ if ao.UserID != "" {
+ return nil, ErrUserIDWithToken
+ }
+ if ao.DomainID != "" {
+ return nil, ErrDomainIDWithToken
+ }
+ if ao.DomainName != "" {
+ return nil, ErrDomainNameWithToken
+ }
+
+ // Configure the request for Token authentication.
+ req.Auth.Identity.Methods = []string{"token"}
+ req.Auth.Identity.Token = &tokenReq{
+ ID: c.TokenID,
+ }
+ } else {
+ // If no password or token ID are available, authentication can't continue.
+ return nil, ErrMissingPassword
+ }
+ } else {
+ // Password authentication.
+ req.Auth.Identity.Methods = []string{"password"}
+
+ // At least one of Username and UserID must be specified.
+ if ao.Username == "" && ao.UserID == "" {
+ return nil, ErrUsernameOrUserID
+ }
+
+ if ao.Username != "" {
+ // If Username is provided, UserID may not be provided.
+ if ao.UserID != "" {
+ return nil, ErrUsernameOrUserID
+ }
+
+ // Either DomainID or DomainName must also be specified.
+ if ao.DomainID == "" && ao.DomainName == "" {
+ return nil, ErrDomainIDOrDomainName
+ }
+
+ if ao.DomainID != "" {
+ if ao.DomainName != "" {
+ return nil, ErrDomainIDOrDomainName
+ }
+
+ // Configure the request for Username and Password authentication with a DomainID.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &ao.Username,
+ Password: ao.Password,
+ Domain: &domainReq{ID: &ao.DomainID},
+ },
+ }
+ }
+
+ if ao.DomainName != "" {
+ // Configure the request for Username and Password authentication with a DomainName.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{
+ Name: &ao.Username,
+ Password: ao.Password,
+ Domain: &domainReq{Name: &ao.DomainName},
+ },
+ }
+ }
+ }
+
+ if ao.UserID != "" {
+ // If UserID is specified, neither DomainID nor DomainName may be.
+ if ao.DomainID != "" {
+ return nil, ErrDomainIDWithUserID
+ }
+ if ao.DomainName != "" {
+ return nil, ErrDomainNameWithUserID
+ }
+
+ // Configure the request for UserID and Password authentication.
+ req.Auth.Identity.Password = &passwordReq{
+ User: userReq{ID: &ao.UserID},
+ }
+ }
+ }
+
+ // Add a "scope" element if a Scope has been provided.
+ if scope != nil {
+ if scope.ProjectName != "" {
+ // ProjectName provided: either DomainID or DomainName must also be supplied.
+ // ProjectID may not be supplied.
+ if scope.DomainID == "" && scope.DomainName == "" {
+ return nil, ErrScopeDomainIDOrDomainName
+ }
+ if scope.ProjectID != "" {
+ return nil, ErrScopeProjectIDOrProjectName
+ }
+
+ if scope.DomainID != "" {
+ // ProjectName + DomainID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{ID: &scope.DomainID},
+ },
+ }
+ }
+
+ if scope.DomainName != "" {
+ // ProjectName + DomainName
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{
+ Name: &scope.ProjectName,
+ Domain: &domainReq{Name: &scope.DomainName},
+ },
+ }
+ }
+ } else if scope.ProjectID != "" {
+ // ProjectID provided. ProjectName, DomainID, and DomainName may not be provided.
+ if scope.ProjectName != "" {
+ return nil, ErrScopeProjectIDOrProjectName
+ }
+ if scope.DomainID != "" {
+ return nil, ErrScopeProjectIDAlone
+ }
+ if scope.DomainName != "" {
+ return nil, ErrScopeProjectIDAlone
+ }
+
+ // ProjectID
+ req.Auth.Scope = &scopeReq{
+ Project: &projectReq{ID: &scope.ProjectID},
+ }
+ } else if scope.DomainID != "" {
+ // DomainID provided. ProjectID, ProjectName, and DomainName may not be provided.
+ if scope.DomainName != "" {
+ return nil, ErrScopeDomainIDOrDomainName
+ }
+
+ // DomainID
+ req.Auth.Scope = &scopeReq{
+ Domain: &domainReq{ID: &scope.DomainID},
+ }
+ } else if scope.DomainName != "" {
+ return nil, ErrScopeDomainName
+ } else {
+ return nil, ErrScopeEmpty
+ }
+ }
+
+ var resp TokenCreateResult
+ perigee.Post(getTokenURL(c), perigee.Options{
+ ReqBody: &req,
+ Results: &resp,
+ OkCodes: []int{201},
+ })
+ return &resp, nil
+}
diff --git a/openstack/identity/v3/tokens/urls.go b/openstack/identity/v3/tokens/urls.go
new file mode 100644
index 0000000..2c100ab
--- /dev/null
+++ b/openstack/identity/v3/tokens/urls.go
@@ -0,0 +1,7 @@
+package tokens
+
+import identity "github.com/rackspace/gophercloud/openstack/identity/v3"
+
+func getTokenURL(c *identity.Client) string {
+ return c.ServiceURL("auth", "tokens")
+}