Merge pull request #468 from DSpeichert/role_assignments

[rfr] Keystone Identity /v3/role_assignments
diff --git a/openstack/identity/v3/roles/doc.go b/openstack/identity/v3/roles/doc.go
new file mode 100644
index 0000000..bdbc674
--- /dev/null
+++ b/openstack/identity/v3/roles/doc.go
@@ -0,0 +1,3 @@
+// Package roles provides information and interaction with the roles API
+// resource for the OpenStack Identity service.
+package roles
diff --git a/openstack/identity/v3/roles/requests.go b/openstack/identity/v3/roles/requests.go
new file mode 100644
index 0000000..d95c1e5
--- /dev/null
+++ b/openstack/identity/v3/roles/requests.go
@@ -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)
+}
diff --git a/openstack/identity/v3/roles/requests_test.go b/openstack/identity/v3/roles/requests_test.go
new file mode 100644
index 0000000..d62dbff
--- /dev/null
+++ b/openstack/identity/v3/roles/requests_test.go
@@ -0,0 +1,104 @@
+package roles
+
+import (
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+
+	"github.com/rackspace/gophercloud/pagination"
+	"github.com/rackspace/gophercloud/testhelper"
+	"github.com/rackspace/gophercloud/testhelper/client"
+)
+
+func TestListSinglePage(t *testing.T) {
+	testhelper.SetupHTTP()
+	defer testhelper.TeardownHTTP()
+
+	testhelper.Mux.HandleFunc("/role_assignments", func(w http.ResponseWriter, r *http.Request) {
+		testhelper.TestMethod(t, r, "GET")
+		testhelper.TestHeader(t, r, "X-Auth-Token", client.TokenID)
+
+		w.Header().Add("Content-Type", "application/json")
+		fmt.Fprintf(w, `
+			{
+                "role_assignments": [
+                    {
+                        "links": {
+                            "assignment": "http://identity:35357/v3/domains/161718/users/313233/roles/123456"
+                        },
+                        "role": {
+                            "id": "123456"
+                        },
+                        "scope": {
+                            "domain": {
+                                "id": "161718"
+                            }
+                        },
+                        "user": {
+                            "id": "313233"
+                        }
+                    },
+                    {
+                        "links": {
+                            "assignment": "http://identity:35357/v3/projects/456789/groups/101112/roles/123456",
+                            "membership": "http://identity:35357/v3/groups/101112/users/313233"
+                        },
+                        "role": {
+                            "id": "123456"
+                        },
+                        "scope": {
+                            "project": {
+                                "id": "456789"
+                            }
+                        },
+                        "user": {
+                            "id": "313233"
+                        }
+                    }
+                ],
+                "links": {
+                    "self": "http://identity:35357/v3/role_assignments?effective",
+                    "previous": null,
+                    "next": null
+                }
+            }
+		`)
+	})
+
+	count := 0
+	err := ListAssignments(client.ServiceClient(), ListAssignmentsOpts{}).EachPage(func(page pagination.Page) (bool, error) {
+		count++
+		actual, err := ExtractRoleAssignments(page)
+		if err != nil {
+			return false, err
+		}
+
+		expected := []RoleAssignment{
+			RoleAssignment{
+				Role:  Role{ID: "123456"},
+				Scope: Scope{Domain: Domain{ID: "161718"}},
+				User:  User{ID: "313233"},
+				Group: Group{},
+			},
+			RoleAssignment{
+				Role:  Role{ID: "123456"},
+				Scope: Scope{Project: Project{ID: "456789"}},
+				User:  User{ID: "313233"},
+				Group: Group{},
+			},
+		}
+
+		if !reflect.DeepEqual(expected, actual) {
+			t.Errorf("Expected %#v, got %#v", expected, actual)
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Errorf("Unexpected error while paging: %v", err)
+	}
+	if count != 1 {
+		t.Errorf("Expected 1 page, got %d", count)
+	}
+}
diff --git a/openstack/identity/v3/roles/results.go b/openstack/identity/v3/roles/results.go
new file mode 100644
index 0000000..d25abd2
--- /dev/null
+++ b/openstack/identity/v3/roles/results.go
@@ -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
+}
diff --git a/openstack/identity/v3/roles/urls.go b/openstack/identity/v3/roles/urls.go
new file mode 100644
index 0000000..b009340
--- /dev/null
+++ b/openstack/identity/v3/roles/urls.go
@@ -0,0 +1,7 @@
+package roles
+
+import "github.com/rackspace/gophercloud"
+
+func listAssignmentsURL(client *gophercloud.ServiceClient) string {
+	return client.ServiceURL("role_assignments")
+}
diff --git a/openstack/identity/v3/roles/urls_test.go b/openstack/identity/v3/roles/urls_test.go
new file mode 100644
index 0000000..04679da
--- /dev/null
+++ b/openstack/identity/v3/roles/urls_test.go
@@ -0,0 +1,15 @@
+package roles
+
+import (
+	"testing"
+
+	"github.com/rackspace/gophercloud"
+)
+
+func TestListAssignmentsURL(t *testing.T) {
+	client := gophercloud.ServiceClient{Endpoint: "http://localhost:5000/v3/"}
+	url := listAssignmentsURL(&client)
+	if url != "http://localhost:5000/v3/role_assignments" {
+		t.Errorf("Unexpected list URL generated: [%s]", url)
+	}
+}