Move deps from _workspace/ to vendor/

godep restore
pushd $GOPATH/src/github.com/appc/spec
git co master
popd
go get go4.org/errorutil
rm -rf Godeps
godep save ./...
git add vendor
git add -f $(git ls-files --other vendor/)
git co -- Godeps/LICENSES Godeps/.license_file_state Godeps/OWNERS
This commit is contained in:
Tim Hockin
2016-05-08 20:30:21 -07:00
parent 899f9b4e31
commit 3c0c5ed4e0
4400 changed files with 16739 additions and 376 deletions

View File

@@ -0,0 +1,16 @@
// Package roles provides functionality to interact with and control roles on
// the API.
//
// A role represents a personality that a user can assume when performing a
// specific set of operations. If a role includes a set of rights and
// privileges, a user assuming that role inherits those rights and privileges.
//
// When a token is generated, the list of roles that user can assume is returned
// back to them. Services that are being called by that user determine how they
// interpret the set of roles a user has and to which operations or resources
// each role grants access.
//
// It is up to individual services such as Compute or Image to assign meaning
// to these roles. As far as the Identity service is concerned, a role is an
// arbitrary name assigned by the user.
package roles

View File

@@ -0,0 +1,48 @@
package roles
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListRoleResponse(t *testing.T) {
th.Mux.HandleFunc("/OS-KSADM/roles", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"roles": [
{
"id": "123",
"name": "compute:admin",
"description": "Nova Administrator"
}
]
}
`)
})
}
func MockAddUserRoleResponse(t *testing.T) {
th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusCreated)
})
}
func MockDeleteUserRoleResponse(t *testing.T) {
th.Mux.HandleFunc("/tenants/{tenant_id}/users/{user_id}/roles/OS-KSADM/{role_id}", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}

View File

@@ -0,0 +1,33 @@
package roles
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// List is the operation responsible for listing all available global roles
// that a user can adopt.
func List(client *gophercloud.ServiceClient) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return RolePage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, rootURL(client), createPage)
}
// AddUserRole is the operation responsible for assigning a particular role to
// a user. This is confined to the scope of the user's tenant - so the tenant
// ID is a required argument.
func AddUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
var result UserRoleResult
_, result.Err = client.Put(userRoleURL(client, tenantID, userID, roleID), nil, nil, nil)
return result
}
// DeleteUserRole is the operation responsible for deleting a particular role
// from a user. This is confined to the scope of the user's tenant - so the
// tenant ID is a required argument.
func DeleteUserRole(client *gophercloud.ServiceClient, tenantID, userID, roleID string) UserRoleResult {
var result UserRoleResult
_, result.Err = client.Delete(userRoleURL(client, tenantID, userID, roleID), nil)
return result
}

View File

@@ -0,0 +1,53 @@
package roles
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Role represents an API role resource.
type Role struct {
// The unique ID for the role.
ID string
// The human-readable name of the role.
Name string
// The description of the role.
Description string
// The associated service for this role.
ServiceID string
}
// RolePage is a single page of a user Role collection.
type RolePage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a page of Tenants contains any results.
func (page RolePage) IsEmpty() (bool, error) {
users, err := ExtractRoles(page)
if err != nil {
return false, err
}
return len(users) == 0, nil
}
// ExtractRoles returns a slice of roles contained in a single page of results.
func ExtractRoles(page pagination.Page) ([]Role, error) {
casted := page.(RolePage).Body
var response struct {
Roles []Role `mapstructure:"roles"`
}
err := mapstructure.Decode(casted, &response)
return response.Roles, err
}
// UserRoleResult represents the result of either an AddUserRole or
// a DeleteUserRole operation.
type UserRoleResult struct {
gophercloud.ErrResult
}

View File

@@ -0,0 +1,21 @@
package roles
import "github.com/rackspace/gophercloud"
const (
ExtPath = "OS-KSADM"
RolePath = "roles"
UserPath = "users"
)
func resourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(ExtPath, RolePath, id)
}
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(ExtPath, RolePath)
}
func userRoleURL(c *gophercloud.ServiceClient, tenantID, userID, roleID string) string {
return c.ServiceURL("tenants", tenantID, UserPath, userID, RolePath, ExtPath, roleID)
}

View File

@@ -0,0 +1,52 @@
package extensions
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
common "github.com/rackspace/gophercloud/openstack/common/extensions"
"github.com/rackspace/gophercloud/pagination"
)
// ExtensionPage is a single page of Extension results.
type ExtensionPage struct {
common.ExtensionPage
}
// IsEmpty returns true if the current page contains at least one Extension.
func (page ExtensionPage) IsEmpty() (bool, error) {
is, err := ExtractExtensions(page)
if err != nil {
return true, err
}
return len(is) == 0, nil
}
// ExtractExtensions accepts a Page struct, specifically an ExtensionPage struct, and extracts the
// elements into a slice of Extension structs.
func ExtractExtensions(page pagination.Page) ([]common.Extension, error) {
// Identity v2 adds an intermediate "values" object.
var resp struct {
Extensions struct {
Values []common.Extension `mapstructure:"values"`
} `mapstructure:"extensions"`
}
err := mapstructure.Decode(page.(ExtensionPage).Body, &resp)
return resp.Extensions.Values, err
}
// Get retrieves information for a specific extension using its alias.
func Get(c *gophercloud.ServiceClient, alias string) common.GetResult {
return common.Get(c, alias)
}
// List returns a Pager which allows you to iterate over the full collection of extensions.
// It does not accept query parameters.
func List(c *gophercloud.ServiceClient) pagination.Pager {
return common.List(c).WithPageCreator(func(r pagination.PageResult) pagination.Page {
return ExtensionPage{
ExtensionPage: common.ExtensionPage{SinglePageBase: pagination.SinglePageBase(r)},
}
})
}

View File

@@ -0,0 +1,3 @@
// Package extensions provides information and interaction with the
// different extensions available for the OpenStack Identity service.
package extensions

View File

@@ -0,0 +1,60 @@
// +build fixtures
package extensions
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput provides a single Extension result. It differs from the delegated implementation
// by the introduction of an intermediate "values" member.
const ListOutput = `
{
"extensions": {
"values": [
{
"updated": "2013-01-20T00:00:00-00:00",
"name": "Neutron Service Type Management",
"links": [],
"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
"alias": "service-type",
"description": "API for retrieving service providers for Neutron advanced services"
}
]
}
}
`
// HandleListExtensionsSuccessfully creates an HTTP handler that returns ListOutput for a List
// call.
func HandleListExtensionsSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/extensions", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Add("Content-Type", "application/json")
fmt.Fprintf(w, `
{
"extensions": {
"values": [
{
"updated": "2013-01-20T00:00:00-00:00",
"name": "Neutron Service Type Management",
"links": [],
"namespace": "http://docs.openstack.org/ext/neutron/service-type/api/v1.0",
"alias": "service-type",
"description": "API for retrieving service providers for Neutron advanced services"
}
]
}
}
`)
})
}

View File

@@ -0,0 +1,7 @@
// Package tenants provides information and interaction with the
// tenants API resource for the OpenStack Identity service.
//
// See http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
// and http://developer.openstack.org/api-ref-identity-v2.html#admin-tenants
// for more information.
package tenants

View File

@@ -0,0 +1,65 @@
// +build fixtures
package tenants
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
"github.com/rackspace/gophercloud/testhelper/client"
)
// ListOutput provides a single page of Tenant results.
const ListOutput = `
{
"tenants": [
{
"id": "1234",
"name": "Red Team",
"description": "The team that is red",
"enabled": true
},
{
"id": "9876",
"name": "Blue Team",
"description": "The team that is blue",
"enabled": false
}
]
}
`
// RedTeam is a Tenant fixture.
var RedTeam = Tenant{
ID: "1234",
Name: "Red Team",
Description: "The team that is red",
Enabled: true,
}
// BlueTeam is a Tenant fixture.
var BlueTeam = Tenant{
ID: "9876",
Name: "Blue Team",
Description: "The team that is blue",
Enabled: false,
}
// ExpectedTenantSlice is the slice of tenants expected to be returned from ListOutput.
var ExpectedTenantSlice = []Tenant{RedTeam, BlueTeam}
// HandleListTenantsSuccessfully creates an HTTP handler at `/tenants` on the test handler mux that
// responds with a list of two tenants.
func HandleListTenantsSuccessfully(t *testing.T) {
th.Mux.HandleFunc("/tenants", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "X-Auth-Token", client.TokenID)
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, ListOutput)
})
}

View File

@@ -0,0 +1,33 @@
package tenants
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ListOpts filters the Tenants that are returned by the List call.
type ListOpts struct {
// Marker is the ID of the last Tenant on the previous page.
Marker string `q:"marker"`
// Limit specifies the page size.
Limit int `q:"limit"`
}
// List enumerates the Tenants to which the current token has access.
func List(client *gophercloud.ServiceClient, opts *ListOpts) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return TenantPage{pagination.LinkedPageBase{PageResult: r}}
}
url := listURL(client)
if opts != nil {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return pagination.Pager{Err: err}
}
url += q.String()
}
return pagination.NewPager(client, url, createPage)
}

View File

@@ -0,0 +1,62 @@
package tenants
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// Tenant is a grouping of users in the identity service.
type Tenant struct {
// ID is a unique identifier for this tenant.
ID string `mapstructure:"id"`
// Name is a friendlier user-facing name for this tenant.
Name string `mapstructure:"name"`
// Description is a human-readable explanation of this Tenant's purpose.
Description string `mapstructure:"description"`
// Enabled indicates whether or not a tenant is active.
Enabled bool `mapstructure:"enabled"`
}
// TenantPage is a single page of Tenant results.
type TenantPage struct {
pagination.LinkedPageBase
}
// IsEmpty determines whether or not a page of Tenants contains any results.
func (page TenantPage) IsEmpty() (bool, error) {
tenants, err := ExtractTenants(page)
if err != nil {
return false, err
}
return len(tenants) == 0, nil
}
// NextPageURL extracts the "next" link from the tenants_links section of the result.
func (page TenantPage) NextPageURL() (string, error) {
type resp struct {
Links []gophercloud.Link `mapstructure:"tenants_links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return gophercloud.ExtractNextURL(r.Links)
}
// ExtractTenants returns a slice of Tenants contained in a single page of results.
func ExtractTenants(page pagination.Page) ([]Tenant, error) {
casted := page.(TenantPage).Body
var response struct {
Tenants []Tenant `mapstructure:"tenants"`
}
err := mapstructure.Decode(casted, &response)
return response.Tenants, err
}

View File

@@ -0,0 +1,7 @@
package tenants
import "github.com/rackspace/gophercloud"
func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("tenants")
}

View File

@@ -0,0 +1,5 @@
// Package tokens provides information and interaction with the token API
// resource for the OpenStack Identity service.
// For more information, see:
// http://developer.openstack.org/api-ref-identity-v2.html#identity-auth-v2
package tokens

View File

@@ -0,0 +1,30 @@
package tokens
import (
"errors"
"fmt"
)
var (
// ErrUserIDProvided is returned if you attempt to authenticate with a UserID.
ErrUserIDProvided = unacceptedAttributeErr("UserID")
// ErrAPIKeyProvided is returned if you attempt to authenticate with an APIKey.
ErrAPIKeyProvided = unacceptedAttributeErr("APIKey")
// ErrDomainIDProvided is returned if you attempt to authenticate with a DomainID.
ErrDomainIDProvided = unacceptedAttributeErr("DomainID")
// ErrDomainNameProvided is returned if you attempt to authenticate with a DomainName.
ErrDomainNameProvided = unacceptedAttributeErr("DomainName")
// ErrUsernameRequired is returned if you attempt to authenticate without a Username.
ErrUsernameRequired = errors.New("You must supply a Username in your AuthOptions.")
// ErrPasswordRequired is returned if you don't provide a password.
ErrPasswordRequired = errors.New("Please supply a Password in your AuthOptions.")
)
func unacceptedAttributeErr(attribute string) error {
return fmt.Errorf("The base Identity V2 API does not accept authentication by %s", attribute)
}

View File

@@ -0,0 +1,195 @@
// +build fixtures
package tokens
import (
"fmt"
"net/http"
"testing"
"time"
"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
th "github.com/rackspace/gophercloud/testhelper"
thclient "github.com/rackspace/gophercloud/testhelper/client"
)
// ExpectedToken is the token that should be parsed from TokenCreationResponse.
var ExpectedToken = &Token{
ID: "aaaabbbbccccdddd",
ExpiresAt: time.Date(2014, time.January, 31, 15, 30, 58, 0, time.UTC),
Tenant: tenants.Tenant{
ID: "fc394f2ab2df4114bde39905f800dc57",
Name: "test",
Description: "There are many tenants. This one is yours.",
Enabled: true,
},
}
// ExpectedServiceCatalog is the service catalog that should be parsed from TokenCreationResponse.
var ExpectedServiceCatalog = &ServiceCatalog{
Entries: []CatalogEntry{
CatalogEntry{
Name: "inscrutablewalrus",
Type: "something",
Endpoints: []Endpoint{
Endpoint{
PublicURL: "http://something0:1234/v2/",
Region: "region0",
},
Endpoint{
PublicURL: "http://something1:1234/v2/",
Region: "region1",
},
},
},
CatalogEntry{
Name: "arbitrarypenguin",
Type: "else",
Endpoints: []Endpoint{
Endpoint{
PublicURL: "http://else0:4321/v3/",
Region: "region0",
},
},
},
},
}
// ExpectedUser is the token that should be parsed from TokenGetResponse.
var ExpectedUser = &User{
ID: "a530fefc3d594c4ba2693a4ecd6be74e",
Name: "apiserver",
Roles: []Role{{"member"}, {"service"}},
UserName: "apiserver",
}
// TokenCreationResponse is a JSON response that contains ExpectedToken and ExpectedServiceCatalog.
const TokenCreationResponse = `
{
"access": {
"token": {
"issued_at": "2014-01-30T15:30:58.000000Z",
"expires": "2014-01-31T15:30:58Z",
"id": "aaaabbbbccccdddd",
"tenant": {
"description": "There are many tenants. This one is yours.",
"enabled": true,
"id": "fc394f2ab2df4114bde39905f800dc57",
"name": "test"
}
},
"serviceCatalog": [
{
"endpoints": [
{
"publicURL": "http://something0:1234/v2/",
"region": "region0"
},
{
"publicURL": "http://something1:1234/v2/",
"region": "region1"
}
],
"type": "something",
"name": "inscrutablewalrus"
},
{
"endpoints": [
{
"publicURL": "http://else0:4321/v3/",
"region": "region0"
}
],
"type": "else",
"name": "arbitrarypenguin"
}
]
}
}
`
// TokenGetResponse is a JSON response that contains ExpectedToken and ExpectedUser.
const TokenGetResponse = `
{
"access": {
"token": {
"issued_at": "2014-01-30T15:30:58.000000Z",
"expires": "2014-01-31T15:30:58Z",
"id": "aaaabbbbccccdddd",
"tenant": {
"description": "There are many tenants. This one is yours.",
"enabled": true,
"id": "fc394f2ab2df4114bde39905f800dc57",
"name": "test"
}
},
"serviceCatalog": [],
"user": {
"id": "a530fefc3d594c4ba2693a4ecd6be74e",
"name": "apiserver",
"roles": [
{
"name": "member"
},
{
"name": "service"
}
],
"roles_links": [],
"username": "apiserver"
}
}
}`
// HandleTokenPost expects a POST against a /tokens handler, ensures that the request body has been
// constructed properly given certain auth options, and returns the result.
func HandleTokenPost(t *testing.T, requestJSON string) {
th.Mux.HandleFunc("/tokens", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "Content-Type", "application/json")
th.TestHeader(t, r, "Accept", "application/json")
if requestJSON != "" {
th.TestJSONRequest(t, r, requestJSON)
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, TokenCreationResponse)
})
}
// HandleTokenGet expects a Get against a /tokens handler, ensures that the request body has been
// constructed properly given certain auth options, and returns the result.
func HandleTokenGet(t *testing.T, token string) {
th.Mux.HandleFunc("/tokens/"+token, func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "Accept", "application/json")
th.TestHeader(t, r, "X-Auth-Token", thclient.TokenID)
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, TokenGetResponse)
})
}
// IsSuccessful ensures that a CreateResult was successful and contains the correct token and
// service catalog.
func IsSuccessful(t *testing.T, result CreateResult) {
token, err := result.ExtractToken()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedToken, token)
serviceCatalog, err := result.ExtractServiceCatalog()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedServiceCatalog, serviceCatalog)
}
// GetIsSuccessful ensures that a GetResult was successful and contains the correct token and
// User Info.
func GetIsSuccessful(t *testing.T, result GetResult) {
token, err := result.ExtractToken()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedToken, token)
user, err := result.ExtractUser()
th.AssertNoErr(t, err)
th.CheckDeepEquals(t, ExpectedUser, user)
}

View File

@@ -0,0 +1,99 @@
package tokens
import (
"fmt"
"github.com/rackspace/gophercloud"
)
// AuthOptionsBuilder describes any argument that may be passed to the Create call.
type AuthOptionsBuilder interface {
// ToTokenCreateMap assembles the Create request body, returning an error if parameters are
// missing or inconsistent.
ToTokenCreateMap() (map[string]interface{}, error)
}
// AuthOptions wraps a gophercloud AuthOptions in order to adhere to the AuthOptionsBuilder
// interface.
type AuthOptions struct {
gophercloud.AuthOptions
}
// WrapOptions embeds a root AuthOptions struct in a package-specific one.
func WrapOptions(original gophercloud.AuthOptions) AuthOptions {
return AuthOptions{AuthOptions: original}
}
// ToTokenCreateMap converts AuthOptions into nested maps that can be serialized into a JSON
// request.
func (auth AuthOptions) ToTokenCreateMap() (map[string]interface{}, error) {
// Error out if an unsupported auth option is present.
if auth.UserID != "" {
return nil, ErrUserIDProvided
}
if auth.APIKey != "" {
return nil, ErrAPIKeyProvided
}
if auth.DomainID != "" {
return nil, ErrDomainIDProvided
}
if auth.DomainName != "" {
return nil, ErrDomainNameProvided
}
// Populate the request map.
authMap := make(map[string]interface{})
if auth.Username != "" {
if auth.Password != "" {
authMap["passwordCredentials"] = map[string]interface{}{
"username": auth.Username,
"password": auth.Password,
}
} else {
return nil, ErrPasswordRequired
}
} else if auth.TokenID != "" {
authMap["token"] = map[string]interface{}{
"id": auth.TokenID,
}
} else {
return nil, fmt.Errorf("You must provide either username/password or tenantID/token values.")
}
if auth.TenantID != "" {
authMap["tenantId"] = auth.TenantID
}
if auth.TenantName != "" {
authMap["tenantName"] = auth.TenantName
}
return map[string]interface{}{"auth": authMap}, nil
}
// Create authenticates to the identity service and attempts to acquire a Token.
// If successful, the CreateResult
// Generally, rather than interact with this call directly, end users should call openstack.AuthenticatedClient(),
// which abstracts all of the gory details about navigating service catalogs and such.
func Create(client *gophercloud.ServiceClient, auth AuthOptionsBuilder) CreateResult {
request, err := auth.ToTokenCreateMap()
if err != nil {
return CreateResult{gophercloud.Result{Err: err}}
}
var result CreateResult
_, result.Err = client.Post(CreateURL(client), request, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
return result
}
// Validates and retrieves information for user's token.
func Get(client *gophercloud.ServiceClient, token string) GetResult {
var result GetResult
_, result.Err = client.Get(GetURL(client, token), &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 203},
})
return result
}

View File

@@ -0,0 +1,170 @@
package tokens
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/openstack/identity/v2/tenants"
)
// Token provides only the most basic information related to an authentication token.
type Token struct {
// ID provides the primary means of identifying a user to the OpenStack API.
// OpenStack defines this field as an opaque value, so do not depend on its content.
// It is safe, however, to compare for equality.
ID string
// ExpiresAt provides a timestamp in ISO 8601 format, indicating when the authentication token becomes invalid.
// After this point in time, future API requests made using this authentication token will respond with errors.
// Either the caller will need to reauthenticate manually, or more preferably, the caller should exploit automatic re-authentication.
// See the AuthOptions structure for more details.
ExpiresAt time.Time
// Tenant provides information about the tenant to which this token grants access.
Tenant tenants.Tenant
}
// Authorization need user info which can get from token authentication's response
type Role struct {
Name string `mapstructure:"name"`
}
type User struct {
ID string `mapstructure:"id"`
Name string `mapstructure:"name"`
UserName string `mapstructure:"username"`
Roles []Role `mapstructure:"roles"`
}
// Endpoint represents a single API endpoint offered by a service.
// It provides the public and internal URLs, if supported, along with a region specifier, again if provided.
// The significance of the Region field will depend upon your provider.
//
// In addition, the interface offered by the service will have version information associated with it
// through the VersionId, VersionInfo, and VersionList fields, if provided or supported.
//
// In all cases, fields which aren't supported by the provider and service combined will assume a zero-value ("").
type Endpoint struct {
TenantID string `mapstructure:"tenantId"`
PublicURL string `mapstructure:"publicURL"`
InternalURL string `mapstructure:"internalURL"`
AdminURL string `mapstructure:"adminURL"`
Region string `mapstructure:"region"`
VersionID string `mapstructure:"versionId"`
VersionInfo string `mapstructure:"versionInfo"`
VersionList string `mapstructure:"versionList"`
}
// CatalogEntry provides a type-safe interface to an Identity API V2 service catalog listing.
// Each class of service, such as cloud DNS or block storage services, will have a single
// CatalogEntry representing it.
//
// Note: when looking for the desired service, try, whenever possible, to key off the type field.
// Otherwise, you'll tie the representation of the service to a specific provider.
type CatalogEntry struct {
// Name will contain the provider-specified name for the service.
Name string `mapstructure:"name"`
// Type will contain a type string if OpenStack defines a type for the service.
// Otherwise, for provider-specific services, the provider may assign their own type strings.
Type string `mapstructure:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service.
Endpoints []Endpoint `mapstructure:"endpoints"`
}
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
type ServiceCatalog struct {
Entries []CatalogEntry
}
// CreateResult defers the interpretation of a created token.
// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
type CreateResult struct {
gophercloud.Result
}
// GetResult is the deferred response from a Get call, which is the same with a Created token.
// Use ExtractUser() to interpret it as a User.
type GetResult struct {
CreateResult
}
// ExtractToken returns the just-created Token from a CreateResult.
func (result CreateResult) ExtractToken() (*Token, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Access struct {
Token struct {
Expires string `mapstructure:"expires"`
ID string `mapstructure:"id"`
Tenant tenants.Tenant `mapstructure:"tenant"`
} `mapstructure:"token"`
} `mapstructure:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
expiresTs, err := time.Parse(gophercloud.RFC3339Milli, response.Access.Token.Expires)
if err != nil {
return nil, err
}
return &Token{
ID: response.Access.Token.ID,
ExpiresAt: expiresTs,
Tenant: response.Access.Token.Tenant,
}, nil
}
// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Access struct {
Entries []CatalogEntry `mapstructure:"serviceCatalog"`
} `mapstructure:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &ServiceCatalog{Entries: response.Access.Entries}, nil
}
// createErr quickly packs an error in a CreateResult.
func createErr(err error) CreateResult {
return CreateResult{gophercloud.Result{Err: err}}
}
// ExtractUser returns the User from a GetResult.
func (result GetResult) ExtractUser() (*User, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Access struct {
User User `mapstructure:"user"`
} `mapstructure:"access"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &response.Access.User, nil
}

View File

@@ -0,0 +1,13 @@
package tokens
import "github.com/rackspace/gophercloud"
// CreateURL generates the URL used to create new Tokens.
func CreateURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("tokens")
}
// GetURL generates the URL used to Validate Tokens.
func GetURL(client *gophercloud.ServiceClient, token string) string {
return client.ServiceURL("tokens", token)
}

View File

@@ -0,0 +1 @@
package users

View File

@@ -0,0 +1,163 @@
package users
import (
"fmt"
"net/http"
"testing"
th "github.com/rackspace/gophercloud/testhelper"
fake "github.com/rackspace/gophercloud/testhelper/client"
)
func MockListUserResponse(t *testing.T) {
th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"users":[
{
"id": "u1000",
"name": "John Smith",
"username": "jqsmith",
"email": "john.smith@example.org",
"enabled": true,
"tenant_id": "12345"
},
{
"id": "u1001",
"name": "Jane Smith",
"username": "jqsmith",
"email": "jane.smith@example.org",
"enabled": true,
"tenant_id": "12345"
}
]
}
`)
})
}
func mockCreateUserResponse(t *testing.T) {
th.Mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "POST")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"user": {
"name": "new_user",
"tenant_id": "12345",
"enabled": false,
"email": "new_user@foo.com"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"user": {
"name": "new_user",
"tenant_id": "12345",
"enabled": false,
"email": "new_user@foo.com",
"id": "c39e3de9be2d4c779f1dfd6abacc176d"
}
}
`)
})
}
func mockGetUserResponse(t *testing.T) {
th.Mux.HandleFunc("/users/new_user", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"user": {
"name": "new_user",
"tenant_id": "12345",
"enabled": false,
"email": "new_user@foo.com",
"id": "c39e3de9be2d4c779f1dfd6abacc176d"
}
}
`)
})
}
func mockUpdateUserResponse(t *testing.T) {
th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "PUT")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
th.TestJSONRequest(t, r, `
{
"user": {
"name": "new_name",
"enabled": true,
"email": "new_email@foo.com"
}
}
`)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"user": {
"name": "new_name",
"tenant_id": "12345",
"enabled": true,
"email": "new_email@foo.com",
"id": "c39e3de9be2d4c779f1dfd6abacc176d"
}
}
`)
})
}
func mockDeleteUserResponse(t *testing.T) {
th.Mux.HandleFunc("/users/c39e3de9be2d4c779f1dfd6abacc176d", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "DELETE")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.WriteHeader(http.StatusNoContent)
})
}
func mockListRolesResponse(t *testing.T) {
th.Mux.HandleFunc("/tenants/1d8b6120dcc640fda4fc9194ffc80273/users/c39e3de9be2d4c779f1dfd6abacc176d/roles", func(w http.ResponseWriter, r *http.Request) {
th.TestMethod(t, r, "GET")
th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `
{
"roles": [
{
"id": "9fe2ff9ee4384b1894a90878d3e92bab",
"name": "foo_role"
},
{
"id": "1ea3d56793574b668e85960fbf651e13",
"name": "admin"
}
]
}
`)
})
}

View File

@@ -0,0 +1,161 @@
package users
import (
"errors"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
func List(client *gophercloud.ServiceClient) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return UserPage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, rootURL(client), createPage)
}
// EnabledState represents whether the user is enabled or not.
type EnabledState *bool
// Useful variables to use when creating or updating users.
var (
iTrue = true
iFalse = false
Enabled EnabledState = &iTrue
Disabled EnabledState = &iFalse
)
// CommonOpts are the parameters that are shared between CreateOpts and
// UpdateOpts
type CommonOpts struct {
// Either a name or username is required. When provided, the value must be
// unique or a 409 conflict error will be returned. If you provide a name but
// omit a username, the latter will be set to the former; and vice versa.
Name, Username string
// The ID of the tenant to which you want to assign this user.
TenantID string
// Indicates whether this user is enabled or not.
Enabled EnabledState
// The email address of this user.
Email string
}
// CreateOpts represents the options needed when creating new users.
type CreateOpts CommonOpts
// CreateOptsBuilder describes struct types that can be accepted by the Create call.
type CreateOptsBuilder interface {
ToUserCreateMap() (map[string]interface{}, error)
}
// ToUserCreateMap assembles a request body based on the contents of a CreateOpts.
func (opts CreateOpts) ToUserCreateMap() (map[string]interface{}, error) {
m := make(map[string]interface{})
if opts.Name == "" && opts.Username == "" {
return m, errors.New("Either a Name or Username must be provided")
}
if opts.Name != "" {
m["name"] = opts.Name
}
if opts.Username != "" {
m["username"] = opts.Username
}
if opts.Enabled != nil {
m["enabled"] = &opts.Enabled
}
if opts.Email != "" {
m["email"] = opts.Email
}
if opts.TenantID != "" {
m["tenant_id"] = opts.TenantID
}
return map[string]interface{}{"user": m}, nil
}
// Create is the operation responsible for creating new users.
func Create(client *gophercloud.ServiceClient, opts CreateOptsBuilder) CreateResult {
var res CreateResult
reqBody, err := opts.ToUserCreateMap()
if err != nil {
res.Err = err
return res
}
_, res.Err = client.Post(rootURL(client), reqBody, &res.Body, &gophercloud.RequestOpts{
OkCodes: []int{200, 201},
})
return res
}
// Get requests details on a single user, either by ID.
func Get(client *gophercloud.ServiceClient, id string) GetResult {
var result GetResult
_, result.Err = client.Get(ResourceURL(client, id), &result.Body, nil)
return result
}
// UpdateOptsBuilder allows extensions to add additional attributes to the Update request.
type UpdateOptsBuilder interface {
ToUserUpdateMap() map[string]interface{}
}
// UpdateOpts specifies the base attributes that may be updated on an existing server.
type UpdateOpts CommonOpts
// ToUserUpdateMap formats an UpdateOpts structure into a request body.
func (opts UpdateOpts) ToUserUpdateMap() map[string]interface{} {
m := make(map[string]interface{})
if opts.Name != "" {
m["name"] = opts.Name
}
if opts.Username != "" {
m["username"] = opts.Username
}
if opts.Enabled != nil {
m["enabled"] = &opts.Enabled
}
if opts.Email != "" {
m["email"] = opts.Email
}
if opts.TenantID != "" {
m["tenant_id"] = opts.TenantID
}
return map[string]interface{}{"user": m}
}
// Update is the operation responsible for updating exist users by their UUID.
func Update(client *gophercloud.ServiceClient, id string, opts UpdateOptsBuilder) UpdateResult {
var result UpdateResult
reqBody := opts.ToUserUpdateMap()
_, result.Err = client.Put(ResourceURL(client, id), reqBody, &result.Body, &gophercloud.RequestOpts{
OkCodes: []int{200},
})
return result
}
// Delete is the operation responsible for permanently deleting an API user.
func Delete(client *gophercloud.ServiceClient, id string) DeleteResult {
var result DeleteResult
_, result.Err = client.Delete(ResourceURL(client, id), nil)
return result
}
func ListRoles(client *gophercloud.ServiceClient, tenantID, userID string) pagination.Pager {
createPage := func(r pagination.PageResult) pagination.Page {
return RolePage{pagination.SinglePageBase(r)}
}
return pagination.NewPager(client, listRolesURL(client, tenantID, userID), createPage)
}

View File

@@ -0,0 +1,128 @@
package users
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// User represents a user resource that exists on the API.
type User struct {
// The UUID for this user.
ID string
// The human name for this user.
Name string
// The username for this user.
Username string
// Indicates whether the user is enabled (true) or disabled (false).
Enabled bool
// The email address for this user.
Email string
// The ID of the tenant to which this user belongs.
TenantID string `mapstructure:"tenant_id"`
}
// Role assigns specific responsibilities to users, allowing them to accomplish
// certain API operations whilst scoped to a service.
type Role struct {
// UUID of the role
ID string
// Name of the role
Name string
}
// UserPage is a single page of a User collection.
type UserPage struct {
pagination.SinglePageBase
}
// RolePage is a single page of a user Role collection.
type RolePage struct {
pagination.SinglePageBase
}
// IsEmpty determines whether or not a page of Tenants contains any results.
func (page UserPage) IsEmpty() (bool, error) {
users, err := ExtractUsers(page)
if err != nil {
return false, err
}
return len(users) == 0, nil
}
// ExtractUsers returns a slice of Tenants contained in a single page of results.
func ExtractUsers(page pagination.Page) ([]User, error) {
casted := page.(UserPage).Body
var response struct {
Users []User `mapstructure:"users"`
}
err := mapstructure.Decode(casted, &response)
return response.Users, err
}
// IsEmpty determines whether or not a page of Tenants contains any results.
func (page RolePage) IsEmpty() (bool, error) {
users, err := ExtractRoles(page)
if err != nil {
return false, err
}
return len(users) == 0, nil
}
// ExtractRoles returns a slice of Roles contained in a single page of results.
func ExtractRoles(page pagination.Page) ([]Role, error) {
casted := page.(RolePage).Body
var response struct {
Roles []Role `mapstructure:"roles"`
}
err := mapstructure.Decode(casted, &response)
return response.Roles, err
}
type commonResult struct {
gophercloud.Result
}
// Extract interprets any commonResult as a User, if possible.
func (r commonResult) Extract() (*User, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
User User `mapstructure:"user"`
}
err := mapstructure.Decode(r.Body, &response)
return &response.User, err
}
// CreateResult represents the result of a Create operation
type CreateResult struct {
commonResult
}
// GetResult represents the result of a Get operation
type GetResult struct {
commonResult
}
// UpdateResult represents the result of an Update operation
type UpdateResult struct {
commonResult
}
// DeleteResult represents the result of a Delete operation
type DeleteResult struct {
commonResult
}

View File

@@ -0,0 +1,21 @@
package users
import "github.com/rackspace/gophercloud"
const (
tenantPath = "tenants"
userPath = "users"
rolePath = "roles"
)
func ResourceURL(c *gophercloud.ServiceClient, id string) string {
return c.ServiceURL(userPath, id)
}
func rootURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL(userPath)
}
func listRolesURL(c *gophercloud.ServiceClient, tenantID, userID string) string {
return c.ServiceURL(tenantPath, tenantID, userPath, userID, rolePath)
}

View File

@@ -0,0 +1,6 @@
// Package endpoints provides information and interaction with the service
// endpoints API resource in the OpenStack Identity service.
//
// For more information, see:
// http://developer.openstack.org/api-ref-identity-v3.html#endpoints-v3
package endpoints

View File

@@ -0,0 +1,21 @@
package endpoints
import "fmt"
func requiredAttribute(attribute string) error {
return fmt.Errorf("You must specify %s for this endpoint.", attribute)
}
var (
// ErrAvailabilityRequired is reported if an Endpoint is created without an Availability.
ErrAvailabilityRequired = requiredAttribute("an availability")
// ErrNameRequired is reported if an Endpoint is created without a Name.
ErrNameRequired = requiredAttribute("a name")
// ErrURLRequired is reported if an Endpoint is created without a URL.
ErrURLRequired = requiredAttribute("a URL")
// ErrServiceIDRequired is reported if an Endpoint is created without a ServiceID.
ErrServiceIDRequired = requiredAttribute("a serviceID")
)

View File

@@ -0,0 +1,123 @@
package endpoints
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// EndpointOpts contains the subset of Endpoint attributes that should be used to create or update an Endpoint.
type EndpointOpts struct {
Availability gophercloud.Availability
Name string
Region string
URL string
ServiceID string
}
// Create inserts a new Endpoint into the service catalog.
// Within EndpointOpts, Region may be omitted by being left as "", but all other fields are required.
func Create(client *gophercloud.ServiceClient, opts EndpointOpts) CreateResult {
// Redefined so that Region can be re-typed as a *string, which can be omitted from the JSON output.
type endpoint struct {
Interface string `json:"interface"`
Name string `json:"name"`
Region *string `json:"region,omitempty"`
URL string `json:"url"`
ServiceID string `json:"service_id"`
}
type request struct {
Endpoint endpoint `json:"endpoint"`
}
// Ensure that EndpointOpts is fully populated.
if opts.Availability == "" {
return createErr(ErrAvailabilityRequired)
}
if opts.Name == "" {
return createErr(ErrNameRequired)
}
if opts.URL == "" {
return createErr(ErrURLRequired)
}
if opts.ServiceID == "" {
return createErr(ErrServiceIDRequired)
}
// Populate the request body.
reqBody := request{
Endpoint: endpoint{
Interface: string(opts.Availability),
Name: opts.Name,
URL: opts.URL,
ServiceID: opts.ServiceID,
},
}
reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
var result CreateResult
_, result.Err = client.Post(listURL(client), reqBody, &result.Body, nil)
return result
}
// ListOpts allows finer control over the endpoints returned by a List call.
// All fields are optional.
type ListOpts struct {
Availability gophercloud.Availability `q:"interface"`
ServiceID string `q:"service_id"`
Page int `q:"page"`
PerPage int `q:"per_page"`
}
// List enumerates endpoints in a paginated collection, optionally filtered by ListOpts criteria.
func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
u := listURL(client)
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return pagination.Pager{Err: err}
}
u += q.String()
createPage := func(r pagination.PageResult) pagination.Page {
return EndpointPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, u, createPage)
}
// Update changes an existing endpoint with new data.
// All fields are optional in the provided EndpointOpts.
func Update(client *gophercloud.ServiceClient, endpointID string, opts EndpointOpts) UpdateResult {
type endpoint struct {
Interface *string `json:"interface,omitempty"`
Name *string `json:"name,omitempty"`
Region *string `json:"region,omitempty"`
URL *string `json:"url,omitempty"`
ServiceID *string `json:"service_id,omitempty"`
}
type request struct {
Endpoint endpoint `json:"endpoint"`
}
reqBody := request{Endpoint: endpoint{}}
reqBody.Endpoint.Interface = gophercloud.MaybeString(string(opts.Availability))
reqBody.Endpoint.Name = gophercloud.MaybeString(opts.Name)
reqBody.Endpoint.Region = gophercloud.MaybeString(opts.Region)
reqBody.Endpoint.URL = gophercloud.MaybeString(opts.URL)
reqBody.Endpoint.ServiceID = gophercloud.MaybeString(opts.ServiceID)
var result UpdateResult
_, result.Err = client.Request("PATCH", endpointURL(client, endpointID), gophercloud.RequestOpts{
JSONBody: &reqBody,
JSONResponse: &result.Body,
OkCodes: []int{200},
})
return result
}
// Delete removes an endpoint from the service catalog.
func Delete(client *gophercloud.ServiceClient, endpointID string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(endpointURL(client, endpointID), nil)
return res
}

View File

@@ -0,0 +1,82 @@
package endpoints
import (
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type commonResult struct {
gophercloud.Result
}
// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Endpoint.
// An error is returned if the original call or the extraction failed.
func (r commonResult) Extract() (*Endpoint, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Endpoint `json:"endpoint"`
}
err := mapstructure.Decode(r.Body, &res)
return &res.Endpoint, err
}
// CreateResult is the deferred result of a Create call.
type CreateResult struct {
commonResult
}
// createErr quickly wraps an error in a CreateResult.
func createErr(err error) CreateResult {
return CreateResult{commonResult{gophercloud.Result{Err: err}}}
}
// UpdateResult is the deferred result of an Update call.
type UpdateResult struct {
commonResult
}
// DeleteResult is the deferred result of an Delete call.
type DeleteResult struct {
gophercloud.ErrResult
}
// Endpoint describes the entry point for another service's API.
type Endpoint struct {
ID string `mapstructure:"id" json:"id"`
Availability gophercloud.Availability `mapstructure:"interface" json:"interface"`
Name string `mapstructure:"name" json:"name"`
Region string `mapstructure:"region" json:"region"`
ServiceID string `mapstructure:"service_id" json:"service_id"`
URL string `mapstructure:"url" json:"url"`
}
// EndpointPage is a single page of Endpoint results.
type EndpointPage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if no Endpoints were returned.
func (p EndpointPage) IsEmpty() (bool, error) {
es, err := ExtractEndpoints(p)
if err != nil {
return true, err
}
return len(es) == 0, nil
}
// ExtractEndpoints extracts an Endpoint slice from a Page.
func ExtractEndpoints(page pagination.Page) ([]Endpoint, error) {
var response struct {
Endpoints []Endpoint `mapstructure:"endpoints"`
}
err := mapstructure.Decode(page.(EndpointPage).Body, &response)
return response.Endpoints, err
}

View File

@@ -0,0 +1,11 @@
package endpoints
import "github.com/rackspace/gophercloud"
func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("endpoints")
}
func endpointURL(client *gophercloud.ServiceClient, endpointID string) string {
return client.ServiceURL("endpoints", endpointID)
}

View File

@@ -0,0 +1,3 @@
// Package roles provides information and interaction with the roles API
// resource for the OpenStack Identity service.
package roles

View File

@@ -0,0 +1,50 @@
package roles
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
// ListAssignmentsOptsBuilder allows extensions to add additional parameters to
// the ListAssignments request.
type ListAssignmentsOptsBuilder interface {
ToRolesListAssignmentsQuery() (string, error)
}
// ListAssignmentsOpts allows you to query the ListAssignments method.
// Specify one of or a combination of GroupId, RoleId, ScopeDomainId, ScopeProjectId,
// and/or UserId to search for roles assigned to corresponding entities.
// Effective lists effective assignments at the user, project, and domain level,
// allowing for the effects of group membership.
type ListAssignmentsOpts struct {
GroupId string `q:"group.id"`
RoleId string `q:"role.id"`
ScopeDomainId string `q:"scope.domain.id"`
ScopeProjectId string `q:"scope.project.id"`
UserId string `q:"user.id"`
Effective bool `q:"effective"`
}
// ToRolesListAssignmentsQuery formats a ListAssignmentsOpts into a query string.
func (opts ListAssignmentsOpts) ToRolesListAssignmentsQuery() (string, error) {
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return "", err
}
return q.String(), nil
}
// ListAssignments enumerates the roles assigned to a specified resource.
func ListAssignments(client *gophercloud.ServiceClient, opts ListAssignmentsOptsBuilder) pagination.Pager {
url := listAssignmentsURL(client)
query, err := opts.ToRolesListAssignmentsQuery()
if err != nil {
return pagination.Pager{Err: err}
}
url += query
createPage := func(r pagination.PageResult) pagination.Page {
return RoleAssignmentsPage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, url, createPage)
}

View File

@@ -0,0 +1,81 @@
package roles
import (
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
// RoleAssignment is the result of a role assignments query.
type RoleAssignment struct {
Role Role `json:"role,omitempty"`
Scope Scope `json:"scope,omitempty"`
User User `json:"user,omitempty"`
Group Group `json:"group,omitempty"`
}
type Role struct {
ID string `json:"id,omitempty"`
}
type Scope struct {
Domain Domain `json:"domain,omitempty"`
Project Project `json:"domain,omitempty"`
}
type Domain struct {
ID string `json:"id,omitempty"`
}
type Project struct {
ID string `json:"id,omitempty"`
}
type User struct {
ID string `json:"id,omitempty"`
}
type Group struct {
ID string `json:"id,omitempty"`
}
// RoleAssignmentsPage is a single page of RoleAssignments results.
type RoleAssignmentsPage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if the page contains no results.
func (p RoleAssignmentsPage) IsEmpty() (bool, error) {
roleAssignments, err := ExtractRoleAssignments(p)
if err != nil {
return true, err
}
return len(roleAssignments) == 0, nil
}
// NextPageURL uses the response's embedded link reference to navigate to the next page of results.
func (page RoleAssignmentsPage) NextPageURL() (string, error) {
type resp struct {
Links struct {
Next string `mapstructure:"next"`
} `mapstructure:"links"`
}
var r resp
err := mapstructure.Decode(page.Body, &r)
if err != nil {
return "", err
}
return r.Links.Next, nil
}
// ExtractRoleAssignments extracts a slice of RoleAssignments from a Collection acquired from List.
func ExtractRoleAssignments(page pagination.Page) ([]RoleAssignment, error) {
var response struct {
RoleAssignments []RoleAssignment `mapstructure:"role_assignments"`
}
err := mapstructure.Decode(page.(RoleAssignmentsPage).Body, &response)
return response.RoleAssignments, err
}

View File

@@ -0,0 +1,7 @@
package roles
import "github.com/rackspace/gophercloud"
func listAssignmentsURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("role_assignments")
}

View File

@@ -0,0 +1,3 @@
// Package services provides information and interaction with the services API
// resource for the OpenStack Identity service.
package services

View File

@@ -0,0 +1,77 @@
package services
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
)
type response struct {
Service Service `json:"service"`
}
// Create adds a new service of the requested type to the catalog.
func Create(client *gophercloud.ServiceClient, serviceType string) CreateResult {
type request struct {
Type string `json:"type"`
}
req := request{Type: serviceType}
var result CreateResult
_, result.Err = client.Post(listURL(client), req, &result.Body, nil)
return result
}
// ListOpts allows you to query the List method.
type ListOpts struct {
ServiceType string `q:"type"`
PerPage int `q:"perPage"`
Page int `q:"page"`
}
// List enumerates the services available to a specific user.
func List(client *gophercloud.ServiceClient, opts ListOpts) pagination.Pager {
u := listURL(client)
q, err := gophercloud.BuildQueryString(opts)
if err != nil {
return pagination.Pager{Err: err}
}
u += q.String()
createPage := func(r pagination.PageResult) pagination.Page {
return ServicePage{pagination.LinkedPageBase{PageResult: r}}
}
return pagination.NewPager(client, u, createPage)
}
// Get returns additional information about a service, given its ID.
func Get(client *gophercloud.ServiceClient, serviceID string) GetResult {
var result GetResult
_, result.Err = client.Get(serviceURL(client, serviceID), &result.Body, nil)
return result
}
// Update changes the service type of an existing service.
func Update(client *gophercloud.ServiceClient, serviceID string, serviceType string) UpdateResult {
type request struct {
Type string `json:"type"`
}
req := request{Type: serviceType}
var result UpdateResult
_, result.Err = client.Request("PATCH", serviceURL(client, serviceID), gophercloud.RequestOpts{
JSONBody: &req,
JSONResponse: &result.Body,
OkCodes: []int{200},
})
return result
}
// Delete removes an existing service.
// It either deletes all associated endpoints, or fails until all endpoints are deleted.
func Delete(client *gophercloud.ServiceClient, serviceID string) DeleteResult {
var res DeleteResult
_, res.Err = client.Delete(serviceURL(client, serviceID), nil)
return res
}

View File

@@ -0,0 +1,80 @@
package services
import (
"github.com/rackspace/gophercloud"
"github.com/rackspace/gophercloud/pagination"
"github.com/mitchellh/mapstructure"
)
type commonResult struct {
gophercloud.Result
}
// Extract interprets a GetResult, CreateResult or UpdateResult as a concrete Service.
// An error is returned if the original call or the extraction failed.
func (r commonResult) Extract() (*Service, error) {
if r.Err != nil {
return nil, r.Err
}
var res struct {
Service `json:"service"`
}
err := mapstructure.Decode(r.Body, &res)
return &res.Service, err
}
// CreateResult is the deferred result of a Create call.
type CreateResult struct {
commonResult
}
// GetResult is the deferred result of a Get call.
type GetResult struct {
commonResult
}
// UpdateResult is the deferred result of an Update call.
type UpdateResult struct {
commonResult
}
// DeleteResult is the deferred result of an Delete call.
type DeleteResult struct {
gophercloud.ErrResult
}
// Service is the result of a list or information query.
type Service struct {
Description *string `json:"description,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
// ServicePage is a single page of Service results.
type ServicePage struct {
pagination.LinkedPageBase
}
// IsEmpty returns true if the page contains no results.
func (p ServicePage) IsEmpty() (bool, error) {
services, err := ExtractServices(p)
if err != nil {
return true, err
}
return len(services) == 0, nil
}
// ExtractServices extracts a slice of Services from a Collection acquired from List.
func ExtractServices(page pagination.Page) ([]Service, error) {
var response struct {
Services []Service `mapstructure:"services"`
}
err := mapstructure.Decode(page.(ServicePage).Body, &response)
return response.Services, err
}

View File

@@ -0,0 +1,11 @@
package services
import "github.com/rackspace/gophercloud"
func listURL(client *gophercloud.ServiceClient) string {
return client.ServiceURL("services")
}
func serviceURL(client *gophercloud.ServiceClient, serviceID string) string {
return client.ServiceURL("services", serviceID)
}

View File

@@ -0,0 +1,6 @@
// Package tokens provides information and interaction with the token API
// resource for the OpenStack Identity service.
//
// For more information, see:
// http://developer.openstack.org/api-ref-identity-v3.html#tokens-v3
package tokens

View File

@@ -0,0 +1,72 @@
package tokens
import (
"errors"
"fmt"
)
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")
)

View File

@@ -0,0 +1,281 @@
package tokens
import (
"net/http"
"github.com/rackspace/gophercloud"
)
// 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
}
func subjectTokenHeaders(c *gophercloud.ServiceClient, subjectToken string) map[string]string {
return map[string]string{
"X-Subject-Token": subjectToken,
}
}
// Create authenticates and either generates a new token, or changes the Scope of an existing token.
func Create(c *gophercloud.ServiceClient, options gophercloud.AuthOptions, scope *Scope) CreateResult {
type domainReq struct {
ID *string `json:"id,omitempty"`
Name *string `json:"name,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,omitempty"`
}
type passwordReq struct {
User userReq `json:"user"`
}
type tokenReq struct {
ID string `json:"id"`
}
type identityReq struct {
Methods []string `json:"methods"`
Password *passwordReq `json:"password,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,omitempty"`
}
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 options.APIKey != "" {
return createErr(ErrAPIKeyProvided)
}
if options.TenantID != "" {
return createErr(ErrTenantIDProvided)
}
if options.TenantName != "" {
return createErr(ErrTenantNameProvided)
}
if options.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 options.Username != "" {
return createErr(ErrUsernameWithToken)
}
if options.UserID != "" {
return createErr(ErrUserIDWithToken)
}
if options.DomainID != "" {
return createErr(ErrDomainIDWithToken)
}
if options.DomainName != "" {
return createErr(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 createErr(ErrMissingPassword)
}
} else {
// Password authentication.
req.Auth.Identity.Methods = []string{"password"}
// At least one of Username and UserID must be specified.
if options.Username == "" && options.UserID == "" {
return createErr(ErrUsernameOrUserID)
}
if options.Username != "" {
// If Username is provided, UserID may not be provided.
if options.UserID != "" {
return createErr(ErrUsernameOrUserID)
}
// Either DomainID or DomainName must also be specified.
if options.DomainID == "" && options.DomainName == "" {
return createErr(ErrDomainIDOrDomainName)
}
if options.DomainID != "" {
if options.DomainName != "" {
return createErr(ErrDomainIDOrDomainName)
}
// Configure the request for Username and Password authentication with a DomainID.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &options.Username,
Password: options.Password,
Domain: &domainReq{ID: &options.DomainID},
},
}
}
if options.DomainName != "" {
// Configure the request for Username and Password authentication with a DomainName.
req.Auth.Identity.Password = &passwordReq{
User: userReq{
Name: &options.Username,
Password: options.Password,
Domain: &domainReq{Name: &options.DomainName},
},
}
}
}
if options.UserID != "" {
// If UserID is specified, neither DomainID nor DomainName may be.
if options.DomainID != "" {
return createErr(ErrDomainIDWithUserID)
}
if options.DomainName != "" {
return createErr(ErrDomainNameWithUserID)
}
// Configure the request for UserID and Password authentication.
req.Auth.Identity.Password = &passwordReq{
User: userReq{ID: &options.UserID, Password: options.Password},
}
}
}
// 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 createErr(ErrScopeDomainIDOrDomainName)
}
if scope.ProjectID != "" {
return createErr(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.DomainID != "" {
return createErr(ErrScopeProjectIDAlone)
}
if scope.DomainName != "" {
return createErr(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 createErr(ErrScopeDomainIDOrDomainName)
}
// DomainID
req.Auth.Scope = &scopeReq{
Domain: &domainReq{ID: &scope.DomainID},
}
} else if scope.DomainName != "" {
return createErr(ErrScopeDomainName)
} else {
return createErr(ErrScopeEmpty)
}
}
var result CreateResult
var response *http.Response
response, result.Err = c.Post(tokenURL(c), req, &result.Body, nil)
if result.Err != nil {
return result
}
result.Header = response.Header
return result
}
// Get validates and retrieves information about another token.
func Get(c *gophercloud.ServiceClient, token string) GetResult {
var result GetResult
var response *http.Response
response, result.Err = c.Get(tokenURL(c), &result.Body, &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{200, 203},
})
if result.Err != nil {
return result
}
result.Header = response.Header
return result
}
// Validate determines if a specified token is valid or not.
func Validate(c *gophercloud.ServiceClient, token string) (bool, error) {
response, err := c.Request("HEAD", tokenURL(c), gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
OkCodes: []int{204, 404},
})
if err != nil {
return false, err
}
return response.StatusCode == 204, nil
}
// Revoke immediately makes specified token invalid.
func Revoke(c *gophercloud.ServiceClient, token string) RevokeResult {
var res RevokeResult
_, res.Err = c.Delete(tokenURL(c), &gophercloud.RequestOpts{
MoreHeaders: subjectTokenHeaders(c, token),
})
return res
}

View File

@@ -0,0 +1,139 @@
package tokens
import (
"time"
"github.com/mitchellh/mapstructure"
"github.com/rackspace/gophercloud"
)
// Endpoint represents a single API endpoint offered by a service.
// It matches either a public, internal or admin URL.
// If supported, it contains a region specifier, again if provided.
// The significance of the Region field will depend upon your provider.
type Endpoint struct {
ID string `mapstructure:"id"`
Region string `mapstructure:"region"`
Interface string `mapstructure:"interface"`
URL string `mapstructure:"url"`
}
// CatalogEntry provides a type-safe interface to an Identity API V3 service catalog listing.
// Each class of service, such as cloud DNS or block storage services, could have multiple
// CatalogEntry representing it (one by interface type, e.g public, admin or internal).
//
// Note: when looking for the desired service, try, whenever possible, to key off the type field.
// Otherwise, you'll tie the representation of the service to a specific provider.
type CatalogEntry struct {
// Service ID
ID string `mapstructure:"id"`
// Name will contain the provider-specified name for the service.
Name string `mapstructure:"name"`
// Type will contain a type string if OpenStack defines a type for the service.
// Otherwise, for provider-specific services, the provider may assign their own type strings.
Type string `mapstructure:"type"`
// Endpoints will let the caller iterate over all the different endpoints that may exist for
// the service.
Endpoints []Endpoint `mapstructure:"endpoints"`
}
// ServiceCatalog provides a view into the service catalog from a previous, successful authentication.
type ServiceCatalog struct {
Entries []CatalogEntry
}
// commonResult is the deferred result of a Create or a Get call.
type commonResult struct {
gophercloud.Result
}
// Extract is a shortcut for ExtractToken.
// This function is deprecated and still present for backward compatibility.
func (r commonResult) Extract() (*Token, error) {
return r.ExtractToken()
}
// ExtractToken interprets a commonResult as a Token.
func (r commonResult) ExtractToken() (*Token, error) {
if r.Err != nil {
return nil, r.Err
}
var response struct {
Token struct {
ExpiresAt string `mapstructure:"expires_at"`
} `mapstructure:"token"`
}
var token Token
// Parse the token itself from the stored headers.
token.ID = r.Header.Get("X-Subject-Token")
err := mapstructure.Decode(r.Body, &response)
if err != nil {
return nil, err
}
// Attempt to parse the timestamp.
token.ExpiresAt, err = time.Parse(gophercloud.RFC3339Milli, response.Token.ExpiresAt)
return &token, err
}
// ExtractServiceCatalog returns the ServiceCatalog that was generated along with the user's Token.
func (result CreateResult) ExtractServiceCatalog() (*ServiceCatalog, error) {
if result.Err != nil {
return nil, result.Err
}
var response struct {
Token struct {
Entries []CatalogEntry `mapstructure:"catalog"`
} `mapstructure:"token"`
}
err := mapstructure.Decode(result.Body, &response)
if err != nil {
return nil, err
}
return &ServiceCatalog{Entries: response.Token.Entries}, nil
}
// CreateResult defers the interpretation of a created token.
// Use ExtractToken() to interpret it as a Token, or ExtractServiceCatalog() to interpret it as a service catalog.
type CreateResult struct {
commonResult
}
// createErr quickly creates a CreateResult that reports an error.
func createErr(err error) CreateResult {
return CreateResult{
commonResult: commonResult{Result: gophercloud.Result{Err: err}},
}
}
// GetResult is the deferred response from a Get call.
type GetResult struct {
commonResult
}
// RevokeResult is the deferred response from a Revoke call.
type RevokeResult struct {
commonResult
}
// Token is a string that grants a user access to a controlled set of services in an OpenStack provider.
// Each Token is valid for a set length of time.
type Token struct {
// ID is the issued token.
ID string
// ExpiresAt is the timestamp at which this token will no longer be accepted.
ExpiresAt time.Time
}

View File

@@ -0,0 +1,7 @@
package tokens
import "github.com/rackspace/gophercloud"
func tokenURL(c *gophercloud.ServiceClient) string {
return c.ServiceURL("auth", "tokens")
}