Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Handle version 3 policies for Resource Manager IAM requests ([projects, folders, organziations].getIamPolicy) #77

Merged
merged 1 commit into from
Feb 13, 2020
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
12 changes: 10 additions & 2 deletions plugin/iamutil/iam_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ const (
type Policy struct {
Bindings []*Binding `json:"bindings,omitempty"`
Etag string `json:"etag,omitempty"`
Version int `json:"version,omitempty"`
}

type Binding struct {
Members []string `json:"members,omitempty"`
Role string `json:"role,omitempty"`
Members []string `json:"members,omitempty"`
Role string `json:"role,omitempty"`
Condition *Condition `json:"condition,omitempty"`
}

type Condition struct {
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
Expression string `json:"expression,omitempty"`
}

type PolicyDelta struct {
Expand Down
8 changes: 8 additions & 0 deletions plugin/iamutil/iam_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,15 @@ func (r *parsedIamResource) SetIamPolicyRequest(p *Policy) (req *http.Request, e
return r.constructRequest(&r.config.SetMethod, strings.NewReader(reqJson))
}

var requestPolicyVersion3 = `{"options": {"requestedPolicyVersion": 3}}`

func (r *parsedIamResource) GetIamPolicyRequest() (*http.Request, error) {
// In order to support Resource Manager policies with conditional bindings,
// we need to request the policy version of 3. This request parameter is backwards compatible
// and will return version 1 policies if they are not yet updated to version 3.
if r.config != nil && r.config.Service == "cloudresourcemanager" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a huge fan of this check against a specific service, but I'm ok with adding this for now to unblock - will need to build some things in for conditions, as it varies by service whether the value is included or not.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand, it's definitely a one-off check as there are other services that aren't included that also support IAM conditions.

I wasn't sure how to scrape that information from the docs into the generatedIamResources, perhaps it could be hardcoded in the generation tool as a service map like correctedRequestFormats. And then LatestPolicyVersion could be added as an optional field in IamRestResource which could be used to replace this specific service conditional.

return r.constructRequest(&r.config.GetMethod, strings.NewReader(requestPolicyVersion3))
}
return r.constructRequest(&r.config.GetMethod, nil)
}

Expand Down
168 changes: 168 additions & 0 deletions plugin/iamutil/iam_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,171 @@ func TestParsedIamResource(t *testing.T) {
}
}
}

func TestConditionalIamResource(t *testing.T) {
r := &parsedIamResource{
relativeId: &gcputil.RelativeResourceName{
Name: "projects",
TypeKey: "cloudresourcemanager/projects",
IdTuples: map[string]string{
"projects": "project",
},
OrderedCollectionIds: []string{"cloudresourcemanager", "projects"},
},
config: &IamRestResource{
Name: "projects",
TypeKey: "cloudresourcemanager/projects",
Service: "cloudresourcemanager",
IsPreferredVersion: true,
GetMethod: RestMethod{
HttpMethod: "GET",
BaseURL: "https://cloudresourcemanager.googleapis.com/v1/",
Path: "projects/{resource}:getIamPolicy",
},
SetMethod: RestMethod{
HttpMethod: "POST",
BaseURL: "https://cloudresourcemanager.googleapis.com/v1/",
Path: "projects/{resource}:setIamPolicy",
RequestFormat: `{"policy":%s}`,
},
Parameters: []string{"resource"},
CollectionReplacementKeys: map[string]string{
"projects": "resource"},
},
}

getR, err := r.GetIamPolicyRequest()
if err != nil {
t.Fatalf("Could not construct GetIamPolicyRequest: %v", err)
}
expectedURLBase := "https://cloudresourcemanager.googleapis.com/v1/projects/project"
if getR.URL.String() != expectedURLBase+":getIamPolicy" {
t.Fatalf("expected get request URL %s, got %s", expectedURLBase+":getIamPolicy", getR.URL.String())
}
if getR.Method != "GET" {
t.Fatalf("expected get request method %s, got %s", "GET", getR.Method)
}
if getR.Body == nil {
t.Fatalf("expected non-nil get body")
}
data, err := ioutil.ReadAll(getR.Body)
if err != nil {
t.Fatalf("Error reading data from request body %v", err)
}
var body interface{}
err = json.Unmarshal(data, &body)
if err != nil {
t.Fatalf("Error parsing json from request body %s %v", string(data), err)
}
reqBody, ok := body.(map[string]interface{})
if !ok {
t.Fatalf("Error asserting request body %s", string(data))
}
options, ok := reqBody["options"]
if !ok {
t.Fatalf("Couldn't find options in request body %s", string(data))
}

optionsMap, ok := options.(map[string]interface{})
if !ok {
t.Fatalf("Error asserting options in request body %s", string(data))
}

requestedPolicyVersion, ok := optionsMap["requestedPolicyVersion"]
if !ok {
t.Fatalf("Couldn't find requestedPolicytVersion in options in request body %s", string(data))
}

version, ok := requestedPolicyVersion.(float64)
if !ok {
t.Fatalf("Error asserting requestedPolicyVersion in request body %s", string(data))
}

if version != 3 {
t.Fatalf("requestedPolicyVersion is not 3 in request body %s", string(data))
}

expectedP := &Policy{
Etag: "atag",
Version: 3,
Bindings: []*Binding{
{
Members: []string{"user:[email protected]", "serviceAccount:[email protected]"},
Role: "roles/arole",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it allows you to set this role against the API?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condition: &Condition{
Title: "test",
Description: "",
Expression: "a==b",
},
},
{
Members: []string{"user:[email protected]"},
Role: "roles/anotherrole",
},
},
}
setR, err := r.SetIamPolicyRequest(expectedP)
if err != nil {
t.Fatalf("Could not construct SetIamPolicyRequest: %v", err)
}

if setR.URL.String() != expectedURLBase+":setIamPolicy" {
t.Fatalf("expected set request URL %s, got %s", expectedURLBase+":setIamPolicy", getR.URL.String())
}
if setR.Method != "POST" {
t.Fatalf("expected set request method %s, got %s", "POST", getR.Method)
}
if setR.Header.Get("Content-Type") != "application/json" {
t.Fatalf("expected `Content Type = application/json` header in set request, headers: %+v", setR.Header)
}
if setR.Body == nil {
t.Fatalf("expected non-nil set body, actually nil")
}
data, err = ioutil.ReadAll(setR.Body)
if err != nil {
t.Fatalf("unable to read data from set request: %v", err)
}

actual := struct {
P *Policy `json:"policy,omitempty"`
}{}
if err := json.Unmarshal(data, &actual); err != nil {
t.Fatalf("unable to read policy from set request body: %v", err)
}
if actual.P.Etag != expectedP.Etag {
t.Fatalf("mismatch set request policy, expected %s, got %s", expectedP.Etag, actual.P.Etag)
}

if len(actual.P.Bindings) != len(expectedP.Bindings) {
t.Fatalf("mismatch set request policy bindings length, expected %+v, got %+v", expectedP.Bindings, actual.P.Bindings)
}

for i, expectB := range expectedP.Bindings {
actualB := actual.P.Bindings[i]
if expectB.Role != actualB.Role {
t.Errorf("expected bindings[%d] to have role %s, got %s", i, expectB.Role, actualB.Role)
}
if len(expectB.Members) != len(actualB.Members) {
t.Errorf("expected bindings[%d] to have members %+v, got %+v", i, expectB.Members, actualB.Members)
}
for memberI, expectM := range expectB.Members {
if expectM != actualB.Members[memberI] {
t.Errorf("expected bindings[%d], members[%d] to be %s, got %s", i, memberI, expectM, actualB.Members[memberI])
}
}
if expectB.Condition != nil {
if actualB.Condition == nil {
t.Errorf("expected bindings[%d] to have condition %s, got %s", i, expectB.Condition, actualB.Condition)
}
if expectB.Condition.Title != actualB.Condition.Title {
t.Errorf("expected bindings[%d] to have condition titled %s, got %s", i, expectB.Condition.Title, actualB.Condition.Title)
}
if expectB.Condition.Description != actualB.Condition.Description {
t.Errorf("expected bindings[%d] to have condition description %s, got %s", i, expectB.Condition.Description, actualB.Condition.Description)
}
if expectB.Condition.Expression != actualB.Condition.Expression {
t.Errorf("expected bindings[%d] to have condition expression %s, got %s", i, expectB.Condition.Expression, actualB.Condition.Expression)
}
}
}
}