From 9e2a6f67728407214698ba77bcbe2fdbdc1fbed2 Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Wed, 2 Nov 2022 17:50:19 +1100 Subject: [PATCH 1/2] Add OPA support to the policy API's: Updated Api's: - Create Policy: Adds the kind and Query options for OPA and enforcement_level - Update Policy: Adds option to update the Query - List Policy: List allows filtering by kind Adds integration tests for the above --- errors.go | 2 + helper_test.go | 63 ++++++ policy.go | 25 ++- policy_integration_beta_test.go | 349 ++++++++++++++++++++++++++++++++ policy_set.go | 9 + 5 files changed, 445 insertions(+), 3 deletions(-) create mode 100644 policy_integration_beta_test.go diff --git a/errors.go b/errors.go index a6aa90e10..a53769241 100644 --- a/errors.go +++ b/errors.go @@ -199,6 +199,8 @@ var ( ErrRequiredName = errors.New("name is required") + ErrRequiredQuery = errors.New("invalid attribute\n\nQuery can't be blank") + ErrRequiredEnabled = errors.New("enabled is required") ErrRequiredEnforce = errors.New("enforce is required") diff --git a/helper_test.go b/helper_test.go index e587d19b9..9ad64b8ea 100644 --- a/helper_test.go +++ b/helper_test.go @@ -613,6 +613,40 @@ func createPolicy(t *testing.T, client *Client, org *Organization) (*Policy, fun } } +func createPolicyWithOptions(t *testing.T, client *Client, org *Organization, opts *PolicyCreateOptions) (*Policy, func()) { + var orgCleanup func() + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + name := randomString(t) + options := PolicyCreateOptions{ + Name: String(name), + Kind: opts.Kind, + Query: opts.Query, + Enforce: opts.Enforce, + } + + ctx := context.Background() + p, err := client.Policies.Create(ctx, org.Name, options) + if err != nil { + t.Fatal(err) + } + + return p, func() { + if err := client.Policies.Delete(ctx, p.ID); err != nil { + t.Errorf("Error destroying policy! WARNING: Dangling resources\n"+ + "may exist! The full error is shown below.\n\n"+ + "Policy: %s\nError: %s", p.ID, err) + } + + if orgCleanup != nil { + orgCleanup() + } + } +} + func createUploadedPolicy(t *testing.T, client *Client, pass bool, org *Organization) (*Policy, func()) { var orgCleanup func() @@ -642,6 +676,35 @@ func createUploadedPolicy(t *testing.T, client *Client, pass bool, org *Organiza } } +func createUploadedPolicyWithOptions(t *testing.T, client *Client, pass bool, org *Organization, opts *PolicyCreateOptions) (*Policy, func()) { + var orgCleanup func() + + if org == nil { + org, orgCleanup = createOrganization(t, client) + } + + p, pCleanup := createPolicyWithOptions(t, client, org, opts) + + ctx := context.Background() + err := client.Policies.Upload(ctx, p.ID, []byte(fmt.Sprintf("main = rule { %t }", pass))) + if err != nil { + t.Fatal(err) + } + + p, err = client.Policies.Read(ctx, p.ID) + if err != nil { + t.Fatal(err) + } + + return p, func() { + pCleanup() + + if orgCleanup != nil { + orgCleanup() + } + } +} + func createOAuthClient(t *testing.T, client *Client, org *Organization) (*OAuthClient, func()) { var orgCleanup func() diff --git a/policy.go b/policy.go index 3443cb21a..43552bb58 100644 --- a/policy.go +++ b/policy.go @@ -48,9 +48,10 @@ type EnforcementLevel string // List the available enforcement types. const ( - EnforcementAdvisory EnforcementLevel = "advisory" - EnforcementHard EnforcementLevel = "hard-mandatory" - EnforcementSoft EnforcementLevel = "soft-mandatory" + EnforcementAdvisory EnforcementLevel = "advisory" + EnforcementHard EnforcementLevel = "hard-mandatory" + EnforcementSoft EnforcementLevel = "soft-mandatory" + EnforcementMandatory EnforcementLevel = "mandatory" ) // PolicyList represents a list of policies.. @@ -63,6 +64,8 @@ type PolicyList struct { type Policy struct { ID string `jsonapi:"primary,policies"` Name string `jsonapi:"attr,name"` + Kind PolicyKind `jsonapi:"attr,kind"` + Query *string `jsonapi:"attr,query"` Description string `jsonapi:"attr,description"` Enforce []*Enforcement `jsonapi:"attr,enforce"` PolicySetCount int `jsonapi:"attr,policy-set-count"` @@ -90,6 +93,10 @@ type PolicyListOptions struct { // Optional: A search string (partial policy name) used to filter the results. Search string `url:"search[name],omitempty"` + + // **Note: This field is still in BETA and subject to change.** + // Optional: A kind string used to filter the results by the policy kind. + Kind PolicyKind `url:"filter[kind],omitempty"` } // PolicyCreateOptions represents the options for creating a new policy. @@ -103,6 +110,14 @@ type PolicyCreateOptions struct { // Required: The name of the policy. Name *string `jsonapi:"attr,name"` + // **Note: This field is still in BETA and subject to change.** + // Optional: The underlying technology that the policy supports. + Kind PolicyKind `jsonapi:"attr,kind,omitempty"` + + // **Note: This field is still in BETA and subject to change.** + // Optional: The query passed to policy evaluation to determine the result of the policy. Only valid for OPA. + Query *string `jsonapi:"attr,query,omitempty"` + // Optional: A description of the policy's purpose. Description *string `jsonapi:"attr,description,omitempty"` @@ -121,6 +136,10 @@ type PolicyUpdateOptions struct { // Optional: A description of the policy's purpose. Description *string `jsonapi:"attr,description,omitempty"` + // **Note: This field is still in BETA and subject to change.** + // Optional: The query passed to policy evaluation to determine the result of the policy. Only valid for OPA. + Query *string `jsonapi:"attr,query,omitempty"` + // Optional: The enforcements of the policy. Enforce []*EnforcementOptions `jsonapi:"attr,enforce,omitempty"` } diff --git a/policy_integration_beta_test.go b/policy_integration_beta_test.go new file mode 100644 index 000000000..f7100a696 --- /dev/null +++ b/policy_integration_beta_test.go @@ -0,0 +1,349 @@ +package tfe + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPoliciesCreate_Beta(t *testing.T) { + skipIfNotCINode(t) + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + t.Run("with valid options - Sentinel", func(t *testing.T) { + name := randomString(t) + options := PolicyCreateOptions{ + Name: String(name), + Description: String("A sample policy"), + Kind: Sentinel, + Enforce: []*EnforcementOptions{ + { + Path: String(name + ".sentinel"), + Mode: EnforcementMode(EnforcementSoft), + }, + }, + } + + p, err := client.Policies.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.Policies.Read(ctx, p.ID) + require.NoError(t, err) + + for _, item := range []*Policy{ + p, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, *options.Name, item.Name) + assert.Equal(t, options.Kind, item.Kind) + assert.Nil(t, options.Query) + assert.Equal(t, *options.Description, item.Description) + } + }) + + t.Run("with no kind", func(t *testing.T) { + name := randomString(t) + options := PolicyCreateOptions{ + Name: String(name), + Description: String("A sample policy"), + Enforce: []*EnforcementOptions{ + { + Path: String(name + ".sentinel"), + Mode: EnforcementMode(EnforcementSoft), + }, + }, + } + + p, err := client.Policies.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.Policies.Read(ctx, p.ID) + require.NoError(t, err) + + for _, item := range []*Policy{ + p, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, *options.Name, item.Name) + assert.Equal(t, Sentinel, item.Kind) + assert.Equal(t, *options.Description, item.Description) + } + }) + + t.Run("with valid options - OPA", func(t *testing.T) { + name := randomString(t) + options := PolicyCreateOptions{ + Name: String(name), + Description: String("A sample policy"), + Kind: OPA, + Query: String("terraform.main"), + Enforce: []*EnforcementOptions{ + { + Path: String(name + ".rego"), + Mode: EnforcementMode(EnforcementMandatory), + }, + }, + } + + p, err := client.Policies.Create(ctx, orgTest.Name, options) + require.NoError(t, err) + + // Get a refreshed view from the API. + refreshed, err := client.Policies.Read(ctx, p.ID) + require.NoError(t, err) + + for _, item := range []*Policy{ + p, + refreshed, + } { + assert.NotEmpty(t, item.ID) + assert.Equal(t, *options.Name, item.Name) + assert.Equal(t, options.Kind, item.Kind) + assert.Equal(t, *options.Query, *item.Query) + assert.Equal(t, *options.Description, item.Description) + } + }) + + t.Run("when options has an invalid name - OPA", func(t *testing.T) { + p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{ + Name: String(badIdentifier), + Kind: OPA, + Query: String("terraform.main"), + Enforce: []*EnforcementOptions{ + { + Path: String(badIdentifier + ".rego"), + Mode: EnforcementMode(EnforcementAdvisory), + }, + }, + }) + assert.Nil(t, p) + assert.EqualError(t, err, ErrInvalidName.Error()) + }) + + t.Run("when options is missing name - OPA", func(t *testing.T) { + p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{ + Kind: OPA, + Query: String("terraform.main"), + Enforce: []*EnforcementOptions{ + { + Path: String(randomString(t) + ".rego"), + Mode: EnforcementMode(EnforcementSoft), + }, + }, + }) + assert.Nil(t, p) + assert.EqualError(t, err, ErrRequiredName.Error()) + }) + + t.Run("when options is missing query - OPA", func(t *testing.T) { + name := randomString(t) + p, err := client.Policies.Create(ctx, orgTest.Name, PolicyCreateOptions{ + Name: String(name), + Kind: OPA, + Enforce: []*EnforcementOptions{ + { + Path: String(randomString(t) + ".rego"), + Mode: EnforcementMode(EnforcementSoft), + }, + }, + }) + assert.Nil(t, p) + assert.Equal(t, err, ErrRequiredQuery) + }) + + t.Run("when options is missing an enforcement", func(t *testing.T) { + options := PolicyCreateOptions{ + Name: String(randomString(t)), + Kind: OPA, + Query: String("terraform.main"), + } + + p, err := client.Policies.Create(ctx, orgTest.Name, options) + assert.Nil(t, p) + assert.Equal(t, err, ErrRequiredEnforce) + }) + + t.Run("when options is missing enforcement path", func(t *testing.T) { + options := PolicyCreateOptions{ + Name: String(randomString(t)), + Kind: OPA, + Query: String("terraform.main"), + Enforce: []*EnforcementOptions{ + { + Mode: EnforcementMode(EnforcementSoft), + }, + }, + } + + p, err := client.Policies.Create(ctx, orgTest.Name, options) + assert.Nil(t, p) + assert.Equal(t, err, ErrRequiredEnforcementPath) + }) + + t.Run("when options is missing enforcement path", func(t *testing.T) { + name := randomString(t) + options := PolicyCreateOptions{ + Name: String(name), + Kind: OPA, + Query: String("terraform.main"), + Enforce: []*EnforcementOptions{ + { + Path: String(name + ".sentinel"), + }, + }, + } + + p, err := client.Policies.Create(ctx, orgTest.Name, options) + assert.Nil(t, p) + assert.Equal(t, err, ErrRequiredEnforcementMode) + }) + + t.Run("when options has an invalid organization", func(t *testing.T) { + p, err := client.Policies.Create(ctx, badIdentifier, PolicyCreateOptions{ + Name: String("foo"), + }) + assert.Nil(t, p) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) +} + +func TestPoliciesList_Beta(t *testing.T) { + skipIfNotCINode(t) + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + pTest1, pTestCleanup1 := createPolicy(t, client, orgTest) + defer pTestCleanup1() + pTest2, pTestCleanup2 := createPolicy(t, client, orgTest) + defer pTestCleanup2() + opaOptions := &PolicyCreateOptions{ + Kind: OPA, + Query: String("terraform.policy1.deny"), + Enforce: []*EnforcementOptions{ + { + Path: String(".rego"), + Mode: EnforcementMode(EnforcementMandatory), + }, + }, + } + pTest3, pTestCleanup3 := createPolicyWithOptions(t, client, orgTest, opaOptions) + defer pTestCleanup3() + + t.Run("without list options", func(t *testing.T) { + pl, err := client.Policies.List(ctx, orgTest.Name, nil) + require.NoError(t, err) + assert.Contains(t, pl.Items, pTest1) + assert.Contains(t, pl.Items, pTest2) + assert.Contains(t, pl.Items, pTest3) + + assert.Equal(t, 1, pl.CurrentPage) + assert.Equal(t, 3, pl.TotalCount) + }) + + t.Run("with pagination", func(t *testing.T) { + // Request a page number which is out of range. The result should + // be successful, but return no results if the paging options are + // properly passed along. + pl, err := client.Policies.List(ctx, orgTest.Name, &PolicyListOptions{ + ListOptions: ListOptions{ + PageNumber: 999, + PageSize: 100, + }, + }) + require.NoError(t, err) + + assert.Empty(t, pl.Items) + assert.Equal(t, 999, pl.CurrentPage) + assert.Equal(t, 3, pl.TotalCount) + }) + + t.Run("with search", func(t *testing.T) { + // Search by one of the policy's names; we should get only that policy + // and pagination data should reflect the search as well + pl, err := client.Policies.List(ctx, orgTest.Name, &PolicyListOptions{ + Search: pTest1.Name, + }) + require.NoError(t, err) + + assert.Contains(t, pl.Items, pTest1) + assert.NotContains(t, pl.Items, pTest2) + assert.Equal(t, 1, pl.CurrentPage) + assert.Equal(t, 1, pl.TotalCount) + }) + + t.Run("with filter by kind", func(t *testing.T) { + pl, err := client.Policies.List(ctx, orgTest.Name, &PolicyListOptions{ + Kind: OPA, + }) + require.NoError(t, err) + + assert.Contains(t, pl.Items, pTest3) + assert.NotContains(t, pl.Items, pTest1) + assert.NotContains(t, pl.Items, pTest2) + assert.Equal(t, 1, pl.CurrentPage) + assert.Equal(t, 1, pl.TotalCount) + }) + + t.Run("without a valid organization", func(t *testing.T) { + ps, err := client.Policies.List(ctx, badIdentifier, nil) + assert.Nil(t, ps) + assert.EqualError(t, err, ErrInvalidOrg.Error()) + }) +} + +func TestPoliciesUpdate_Beta(t *testing.T) { + skipIfNotCINode(t) + skipIfFreeOnly(t) + skipIfBeta(t) + + client := testClient(t) + ctx := context.Background() + + orgTest, orgTestCleanup := createOrganization(t, client) + defer orgTestCleanup() + + t.Run("with a new query", func(t *testing.T) { + options := &PolicyCreateOptions{ + Description: String("A sample policy"), + Kind: OPA, + Query: String("terraform.main"), + Enforce: []*EnforcementOptions{ + { + Path: String(".rego"), + Mode: EnforcementMode(EnforcementMandatory), + }, + }, + } + pBefore, pBeforeCleanup := createUploadedPolicyWithOptions(t, client, true, orgTest, options) + defer pBeforeCleanup() + + pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{ + Query: String("terraform.policy1.deny"), + }) + require.NoError(t, err) + + assert.Equal(t, pBefore.Name, pAfter.Name) + assert.Equal(t, pBefore.Enforce, pAfter.Enforce) + assert.NotEqual(t, *pBefore.Query, *pAfter.Query) + assert.Equal(t, "terraform.policy1.deny", *pAfter.Query) + }) +} diff --git a/policy_set.go b/policy_set.go index 85207a1f7..756aed640 100644 --- a/policy_set.go +++ b/policy_set.go @@ -10,6 +10,15 @@ import ( // Compile-time proof of interface implementation. var _ PolicySets = (*policySets)(nil) +// PolicyKind is an indicator of the underlying technology that the policy or policy set supports. +// There are two Policykinds documented in the enum. +type PolicyKind string + +const ( + OPA PolicyKind = "opa" + Sentinel PolicyKind = "sentinel" +) + // PolicySets describes all the policy set related methods that the Terraform // Enterprise API supports. // From d4ae7c4c760cdd4a72051b71c121ef880c77bd54 Mon Sep 17 00:00:00 2001 From: mrinalirao Date: Fri, 4 Nov 2022 09:43:17 +1100 Subject: [PATCH 2/2] Modify test helper func + add additional tests and other minor fixes --- CHANGELOG.md | 3 ++- errors.go | 2 +- helper_test.go | 4 ++-- policy.go | 5 ++++- policy_integration_beta_test.go | 21 ++++++++++++++++++--- 5 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 249057164..ac628b5b1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,8 @@ ## Enhancements -* Add OPA support to the Policy Set API's by @mrinalirao [#575](https://github.com/hashicorp/go-tfe/pull/575) +* Add OPA support to the Policy Set APIs by @mrinalirao [#575](https://github.com/hashicorp/go-tfe/pull/575) +* Add OPA support to the Policy APIs by @mrinalirao [#579](https://github.com/hashicorp/go-tfe/pull/579) # v1.12.0 diff --git a/errors.go b/errors.go index a53769241..31371e7da 100644 --- a/errors.go +++ b/errors.go @@ -199,7 +199,7 @@ var ( ErrRequiredName = errors.New("name is required") - ErrRequiredQuery = errors.New("invalid attribute\n\nQuery can't be blank") + ErrRequiredQuery = errors.New("query cannot be empty") ErrRequiredEnabled = errors.New("enabled is required") diff --git a/helper_test.go b/helper_test.go index f9f804daa..b424a5e2e 100644 --- a/helper_test.go +++ b/helper_test.go @@ -614,7 +614,7 @@ func createPolicy(t *testing.T, client *Client, org *Organization) (*Policy, fun } } -func createPolicyWithOptions(t *testing.T, client *Client, org *Organization, opts *PolicyCreateOptions) (*Policy, func()) { +func createPolicyWithOptions(t *testing.T, client *Client, org *Organization, opts PolicyCreateOptions) (*Policy, func()) { var orgCleanup func() if org == nil { @@ -677,7 +677,7 @@ func createUploadedPolicy(t *testing.T, client *Client, pass bool, org *Organiza } } -func createUploadedPolicyWithOptions(t *testing.T, client *Client, pass bool, org *Organization, opts *PolicyCreateOptions) (*Policy, func()) { +func createUploadedPolicyWithOptions(t *testing.T, client *Client, pass bool, org *Organization, opts PolicyCreateOptions) (*Policy, func()) { var orgCleanup func() if org == nil { diff --git a/policy.go b/policy.go index 43552bb58..0baa9530b 100644 --- a/policy.go +++ b/policy.go @@ -111,7 +111,7 @@ type PolicyCreateOptions struct { Name *string `jsonapi:"attr,name"` // **Note: This field is still in BETA and subject to change.** - // Optional: The underlying technology that the policy supports. + // Optional: The underlying technology that the policy supports. Defaults to Sentinel if not specified for PolicyCreate. Kind PolicyKind `jsonapi:"attr,kind,omitempty"` // **Note: This field is still in BETA and subject to change.** @@ -289,6 +289,9 @@ func (o PolicyCreateOptions) valid() error { if !validStringID(o.Name) { return ErrInvalidName } + if o.Kind == OPA && !validString(o.Query) { + return ErrRequiredQuery + } if o.Enforce == nil { return ErrRequiredEnforce } diff --git a/policy_integration_beta_test.go b/policy_integration_beta_test.go index f7100a696..93948186e 100644 --- a/policy_integration_beta_test.go +++ b/policy_integration_beta_test.go @@ -193,7 +193,7 @@ func TestPoliciesCreate_Beta(t *testing.T) { assert.Equal(t, err, ErrRequiredEnforcementPath) }) - t.Run("when options is missing enforcement path", func(t *testing.T) { + t.Run("when options is missing enforcement mode", func(t *testing.T) { name := randomString(t) options := PolicyCreateOptions{ Name: String(name), @@ -235,7 +235,7 @@ func TestPoliciesList_Beta(t *testing.T) { defer pTestCleanup1() pTest2, pTestCleanup2 := createPolicy(t, client, orgTest) defer pTestCleanup2() - opaOptions := &PolicyCreateOptions{ + opaOptions := PolicyCreateOptions{ Kind: OPA, Query: String("terraform.policy1.deny"), Enforce: []*EnforcementOptions{ @@ -322,7 +322,7 @@ func TestPoliciesUpdate_Beta(t *testing.T) { defer orgTestCleanup() t.Run("with a new query", func(t *testing.T) { - options := &PolicyCreateOptions{ + options := PolicyCreateOptions{ Description: String("A sample policy"), Kind: OPA, Query: String("terraform.main"), @@ -346,4 +346,19 @@ func TestPoliciesUpdate_Beta(t *testing.T) { assert.NotEqual(t, *pBefore.Query, *pAfter.Query) assert.Equal(t, "terraform.policy1.deny", *pAfter.Query) }) + + t.Run("update query when kind is not OPA", func(t *testing.T) { + pBefore, pBeforeCleanup := createUploadedPolicy(t, client, true, orgTest) + defer pBeforeCleanup() + + pAfter, err := client.Policies.Update(ctx, pBefore.ID, PolicyUpdateOptions{ + Query: String("terraform.policy1.deny"), + }) + require.NoError(t, err) + + assert.Equal(t, pBefore.Name, pAfter.Name) + assert.Equal(t, pBefore.Enforce, pAfter.Enforce) + assert.Equal(t, Sentinel, pAfter.Kind) + assert.Nil(t, pAfter.Query) + }) }