Skip to content

Commit

Permalink
[SECENG-839] added command to get/set policy settings (#791)
Browse files Browse the repository at this point in the history
  • Loading branch information
sagar-connect authored Oct 4, 2022
1 parent d66718f commit 97049b8
Show file tree
Hide file tree
Showing 6 changed files with 414 additions and 0 deletions.
69 changes: 69 additions & 0 deletions api/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,75 @@ type DecisionRequest struct {
Metadata map[string]interface{} `json:"metadata,omitempty"`
}

// GetSettings calls the GET decision-settings API of policy-service.
func (c Client) GetSettings(ownerID string, context string) (interface{}, error) {
path := fmt.Sprintf("%s/api/v1/owner/%s/context/%s/decision/settings", c.serverUrl, ownerID, context)
req, err := http.NewRequest("GET", path, nil)
if err != nil {
return nil, fmt.Errorf("failed to construct request: %v", err)
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
var payload httpError
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("unexpected status-code: %d", resp.StatusCode)
}
return nil, fmt.Errorf("unexpected status-code: %d - %s", resp.StatusCode, payload.Error)
}

var body interface{}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("failed to decode response body: %v", err)
}

return body, nil
}

// DecisionSettings represents a request to Policy-Service to configure decision settings.
type DecisionSettings struct {
Enabled *bool `json:"enabled,omitempty"`
}

// SetSettings calls the PATCH decision-settings API of policy-service.
func (c Client) SetSettings(ownerID string, context string, request DecisionSettings) (interface{}, error) {
payload, err := json.Marshal(request)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
path := fmt.Sprintf("%s/api/v1/owner/%s/context/%s/decision/settings", c.serverUrl, ownerID, context)
req, err := http.NewRequest("PATCH", path, bytes.NewReader(payload))
if err != nil {
return nil, fmt.Errorf("failed to construct request: %v", err)
}

resp, err := c.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
var payload httpError
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, fmt.Errorf("unexpected status-code: %d", resp.StatusCode)
}
return nil, fmt.Errorf("unexpected status-code: %d - %s", resp.StatusCode, payload.Error)
}

var body interface{}
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
return nil, fmt.Errorf("failed to decode response body: %v", err)
}

return body, nil
}

// MakeDecision sends a requests to Policy-Service public decision endpoint and returns the decision response
func (c Client) MakeDecision(ownerID string, context string, req DecisionRequest) (*cpa.Decision, error) {
payload, err := json.Marshal(req)
Expand Down
162 changes: 162 additions & 0 deletions api/policy/policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -632,3 +632,165 @@ func TestMakeDecision(t *testing.T) {
})
}
}

func TestGetSettings(t *testing.T) {
testcases := []struct {
Name string
OwnerID string
Handler http.HandlerFunc
ExpectedError error
ExpectedSettings interface{}
}{
{
Name: "gets expected response",
OwnerID: "test-owner",
Handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/config/decision/settings")
assert.Equal(t, r.Method, "GET")
assert.Equal(t, r.Header.Get("Circle-Token"), "test-token")
_ = json.NewEncoder(w).Encode(interface{}(`{"enabled": true}`))
},
ExpectedSettings: interface{}(`{"enabled": true}`),
},
{
Name: "unexpected status code",
OwnerID: "test-owner",
Handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(400)
_, _ = io.WriteString(w, `{"error":"that was a bad request!"}`)
},
ExpectedError: errors.New("unexpected status-code: 400 - that was a bad request!"),
},

{
Name: "unexpected status code no body",
OwnerID: "test-owner",
Handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(204)
},
ExpectedError: errors.New("unexpected status-code: 204"),
},
{
Name: "bad decoding",
OwnerID: "test-owner",
Handler: func(w http.ResponseWriter, _ *http.Request) {
_, _ = io.WriteString(w, "not a json response")
},
ExpectedError: errors.New("failed to decode response body: invalid character 'o' in literal null (expecting 'u')"),
},
}

for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
svr := httptest.NewServer(tc.Handler)
defer svr.Close()

client := NewClient(svr.URL, &settings.Config{Token: "test-token", HTTPClient: http.DefaultClient})

settings, err := client.GetSettings(tc.OwnerID, "config")
if tc.ExpectedError == nil {
assert.NilError(t, err)
} else {
assert.Error(t, err, tc.ExpectedError.Error())
return
}

assert.DeepEqual(t, settings, tc.ExpectedSettings)
})
}
}

func TestSetSettings(t *testing.T) {
trueVar := true
falseVar := false

testcases := []struct {
Name string
OwnerID string
Settings DecisionSettings
Handler http.HandlerFunc
ExpectedError error
ExpectedStatus int
ExpectedResponse interface{}
}{
{
Name: "sends expected request (enabled=true)",
OwnerID: "test-owner",
Settings: DecisionSettings{Enabled: &trueVar},
Handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/config/decision/settings")
assert.Equal(t, r.Method, "PATCH")
assert.Equal(t, r.Header.Get("Circle-Token"), "test-token")
var payload map[string]interface{}
assert.NilError(t, json.NewDecoder(r.Body).Decode(&payload))
assert.DeepEqual(t, payload, map[string]interface{}{
"enabled": true,
})
_ = json.NewEncoder(w).Encode(interface{}(`{"enabled": true}`))
},
ExpectedStatus: 200,
ExpectedResponse: interface{}(`{"enabled": true}`),
},
{
Name: "sends expected request (enabled=false)",
OwnerID: "test-owner",
Settings: DecisionSettings{Enabled: &falseVar},
Handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/config/decision/settings")
assert.Equal(t, r.Method, "PATCH")
assert.Equal(t, r.Header.Get("Circle-Token"), "test-token")
var payload map[string]interface{}
assert.NilError(t, json.NewDecoder(r.Body).Decode(&payload))
assert.DeepEqual(t, payload, map[string]interface{}{
"enabled": false,
})
_ = json.NewEncoder(w).Encode(interface{}(`{"enabled": false}`))
},
ExpectedStatus: 200,
ExpectedResponse: interface{}(`{"enabled": false}`),
},
{
Name: "sends expected request (enabled=nil)",
OwnerID: "test-owner",
Settings: DecisionSettings{Enabled: nil},
Handler: func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, r.URL.Path, "/api/v1/owner/test-owner/context/config/decision/settings")
assert.Equal(t, r.Method, "PATCH")
assert.Equal(t, r.Header.Get("Circle-Token"), "test-token")
var payload map[string]interface{}
assert.NilError(t, json.NewDecoder(r.Body).Decode(&payload))
assert.DeepEqual(t, payload, map[string]interface{}{})
_ = json.NewEncoder(w).Encode(interface{}(`{}`))
},
ExpectedStatus: 200,
ExpectedResponse: interface{}(`{}`),
},
{
Name: "unexpected status code",
OwnerID: "test-owner",
Handler: func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(400)
_, _ = io.WriteString(w, `{"error":"that was a bad request!"}`)
},
ExpectedError: errors.New("unexpected status-code: 400 - that was a bad request!"),
},
}

for _, tc := range testcases {
t.Run(tc.Name, func(t *testing.T) {
svr := httptest.NewServer(tc.Handler)
defer svr.Close()

client := NewClient(svr.URL, &settings.Config{Token: "test-token", HTTPClient: http.DefaultClient})

response, err := client.SetSettings(tc.OwnerID, "config", tc.Settings)
if tc.ExpectedError == nil {
assert.NilError(t, err)
} else {
assert.Error(t, err, tc.ExpectedError.Error())
return
}
assert.DeepEqual(t, response, tc.ExpectedResponse)
})
}
}
47 changes: 47 additions & 0 deletions cmd/policy/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,12 +380,59 @@ This group of commands allows the management of polices to be verified against b
return cmd
}()

settings := func() *cobra.Command {
var (
ownerID string
context string
enabled bool
request policy.DecisionSettings
)

cmd := &cobra.Command{

Short: "get/set policy decision settings (To read settings: run command without any settings flags)",
Use: "settings",
RunE: func(cmd *cobra.Command, args []string) error {
client := policy.NewClient(*policyBaseURL, config)

response, err := func() (interface{}, error) {
if cmd.Flag("enabled").Changed {
request.Enabled = &enabled
return client.SetSettings(ownerID, context, request)
}
return client.GetSettings(ownerID, context)
}()
if err != nil {
return fmt.Errorf("failed to run settings : %w", err)
}

if err = prettyJSONEncoder(cmd.OutOrStdout()).Encode(response); err != nil {
return fmt.Errorf("failed to encode settings: %w", err)
}

return nil
},
Args: cobra.ExactArgs(0),
Example: `policy settings --enabled=true`,
}

cmd.Flags().StringVar(&ownerID, "owner-id", "", "the id of the policy's owner")
cmd.Flags().StringVar(&context, "context", "config", "policy context for decision")
cmd.Flags().BoolVar(&enabled, "enabled", false, "enable/disable policy decision evaluation in build pipeline")
if err := cmd.MarkFlagRequired("owner-id"); err != nil {
panic(err)
}

return cmd
}()

cmd.AddCommand(push)
cmd.AddCommand(diff)
cmd.AddCommand(fetch)
cmd.AddCommand(logs)
cmd.AddCommand(decide)
cmd.AddCommand(eval)
cmd.AddCommand(settings)

return cmd
}
Expand Down
Loading

0 comments on commit 97049b8

Please sign in to comment.