From c2f68e6fab4cdaad91d24c026421f9bcaee0133e Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Tue, 28 Apr 2026 11:48:30 -0400 Subject: [PATCH 1/2] feat: add rbac policy management commands --- cli/cmd/policy.go | 15 ++++++ cli/cmd/policy_create.go | 87 +++++++++++++++++++++++++++++++ cli/cmd/policy_get.go | 63 ++++++++++++++++++++++ cli/cmd/policy_ls.go | 40 ++++++++++++++ cli/cmd/policy_rm.go | 55 ++++++++++++++++++++ cli/cmd/policy_update.go | 92 +++++++++++++++++++++++++++++++++ cli/cmd/root.go | 8 +++ cli/print/policies.go | 45 ++++++++++++++++ pkg/kotsclient/policy_create.go | 31 +++++++++++ pkg/kotsclient/policy_delete.go | 12 +++++ pkg/kotsclient/policy_get.go | 23 +++++++++ pkg/kotsclient/policy_update.go | 33 ++++++++++++ 12 files changed, 504 insertions(+) create mode 100644 cli/cmd/policy.go create mode 100644 cli/cmd/policy_create.go create mode 100644 cli/cmd/policy_get.go create mode 100644 cli/cmd/policy_ls.go create mode 100644 cli/cmd/policy_rm.go create mode 100644 cli/cmd/policy_update.go create mode 100644 cli/print/policies.go create mode 100644 pkg/kotsclient/policy_create.go create mode 100644 pkg/kotsclient/policy_delete.go create mode 100644 pkg/kotsclient/policy_get.go create mode 100644 pkg/kotsclient/policy_update.go diff --git a/cli/cmd/policy.go b/cli/cmd/policy.go new file mode 100644 index 000000000..425b68ac4 --- /dev/null +++ b/cli/cmd/policy.go @@ -0,0 +1,15 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +func (r *runners) InitPolicyCommand(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "policy", + Short: "Manage RBAC policies", + Long: "The policy command allows vendors to list, create, update, and remove RBAC policies.", + } + parent.AddCommand(cmd) + return cmd +} diff --git a/cli/cmd/policy_create.go b/cli/cmd/policy_create.go new file mode 100644 index 000000000..896572c81 --- /dev/null +++ b/cli/cmd/policy_create.go @@ -0,0 +1,87 @@ +package cmd + +import ( + "encoding/json" + "os" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/cli/print" + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/spf13/cobra" +) + +func (r *runners) InitPolicyCreate(parent *cobra.Command) *cobra.Command { + var ( + name string + description string + definitionFile string + outputFormat string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create an RBAC policy", + Long: `Create a new RBAC policy from a JSON definition file. + +The definition file must be valid JSON in the following format: + { + "v1": { + "name": "My Policy", + "resources": { + "allowed": ["**/*"], + "denied": [] + } + } + } + +Vendors not on an enterprise plan cannot create policies.`, + Example: ` # Create a policy from a definition file + replicated policy create --name "My Policy" --definition policy.json + + # Create a policy with a description + replicated policy create --name "My Policy" --description "Custom access policy" --definition policy.json`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.policyCreate(name, description, definitionFile, outputFormat) + }, + SilenceUsage: true, + } + parent.AddCommand(cmd) + cmd.Flags().StringVar(&name, "name", "", "Name of the policy") + cmd.Flags().StringVar(&description, "description", "", "Description of the policy") + cmd.Flags().StringVar(&definitionFile, "definition", "", "Path to the JSON file containing the policy definition") + cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.MarkFlagRequired("name") + cmd.MarkFlagRequired("definition") + + return cmd +} + +func (r *runners) policyCreate(name, description, definitionFile, outputFormat string) error { + definition, err := readPolicyDefinition(definitionFile) + if err != nil { + return errors.Wrap(err, "read policy definition") + } + + policy, err := r.kotsAPI.CreatePolicy(name, description, definition) + if err != nil { + if errors.Cause(err) == platformclient.ErrForbidden { + return errors.New("creating policies requires an enterprise plan") + } + return errors.Wrap(err, "create policy") + } + + return print.Policy(outputFormat, r.w, policy) +} + +func readPolicyDefinition(path string) (string, error) { + data, err := os.ReadFile(path) + if err != nil { + return "", errors.Wrap(err, "read file") + } + + if !json.Valid(data) { + return "", errors.New("policy definition file is not valid JSON") + } + + return string(data), nil +} diff --git a/cli/cmd/policy_get.go b/cli/cmd/policy_get.go new file mode 100644 index 000000000..9beac8328 --- /dev/null +++ b/cli/cmd/policy_get.go @@ -0,0 +1,63 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/cli/print" + "github.com/spf13/cobra" +) + +func (r *runners) InitPolicyGet(parent *cobra.Command) *cobra.Command { + var ( + outputFormat string + outputFile string + ) + + cmd := &cobra.Command{ + Use: "get NAME_OR_ID", + Short: "Get an RBAC policy", + Long: "Display details for an RBAC policy. Use --output-file to save the policy definition to a JSON file.", + Example: ` # Get a policy by name + replicated policy get "My Policy" + + # Get a policy and save its definition to a file + replicated policy get "My Policy" --output-file policy.json + + # Get a policy in JSON format + replicated policy get "My Policy" --output json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return r.policyGet(args[0], outputFormat, outputFile) + }, + SilenceUsage: true, + } + parent.AddCommand(cmd) + cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + cmd.Flags().StringVar(&outputFile, "output-file", "", "If set, saves the policy definition to the specified file") + + return cmd +} + +func (r *runners) policyGet(nameOrID, outputFormat, outputFile string) error { + policy, err := r.kotsAPI.GetPolicyByNameOrID(nameOrID) + if err != nil { + return errors.Wrap(err, "get policy") + } + + if outputFile != "" { + b, err := json.MarshalIndent(json.RawMessage(policy.Definition), "", " ") + if err != nil { + return errors.Wrap(err, "marshal policy definition") + } + if err := os.WriteFile(outputFile, append(b, '\n'), 0644); err != nil { + return errors.Wrap(err, "write policy file") + } + fmt.Fprintf(r.w, "Policy definition saved to %s\n", outputFile) + return r.w.Flush() + } + + return print.Policy(outputFormat, r.w, policy) +} diff --git a/cli/cmd/policy_ls.go b/cli/cmd/policy_ls.go new file mode 100644 index 000000000..700b714f1 --- /dev/null +++ b/cli/cmd/policy_ls.go @@ -0,0 +1,40 @@ +package cmd + +import ( + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/cli/print" + "github.com/spf13/cobra" +) + +func (r *runners) InitPolicyList(parent *cobra.Command) *cobra.Command { + var outputFormat string + + cmd := &cobra.Command{ + Use: "ls", + Aliases: []string{"list"}, + Short: "List RBAC policies", + Long: "List all RBAC policies for your team.", + Example: ` # List all policies + replicated policy ls + + # List policies in JSON format + replicated policy ls --output json`, + RunE: func(cmd *cobra.Command, args []string) error { + return r.policyList(outputFormat) + }, + SilenceUsage: true, + } + parent.AddCommand(cmd) + cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + + return cmd +} + +func (r *runners) policyList(outputFormat string) error { + policies, err := r.kotsAPI.ListPolicies() + if err != nil { + return errors.Wrap(err, "list policies") + } + + return print.Policies(outputFormat, r.w, policies) +} diff --git a/cli/cmd/policy_rm.go b/cli/cmd/policy_rm.go new file mode 100644 index 000000000..ddd970aa0 --- /dev/null +++ b/cli/cmd/policy_rm.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/spf13/cobra" +) + +func (r *runners) InitPolicyRm(parent *cobra.Command) *cobra.Command { + cmd := &cobra.Command{ + Use: "rm NAME_OR_ID", + Aliases: []string{"delete"}, + Short: "Remove an RBAC policy", + Long: `Remove an RBAC policy. + +The Admin, Read Only, Sales, and Support policies cannot be removed. +Vendors not on an enterprise plan cannot remove policies.`, + Example: ` # Remove a policy by name + replicated policy rm "My Policy" + + # Remove a policy by ID + replicated policy rm pol_abc123`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return r.policyRm(args[0]) + }, + SilenceUsage: true, + } + parent.AddCommand(cmd) + + return cmd +} + +func (r *runners) policyRm(nameOrID string) error { + policy, err := r.kotsAPI.GetPolicyByNameOrID(nameOrID) + if err != nil { + return errors.Wrap(err, "get policy") + } + + if policy.ReadOnly { + return fmt.Errorf("policy %q is read-only and cannot be removed", policy.Name) + } + + if err := r.kotsAPI.DeletePolicy(policy.ID); err != nil { + if errors.Cause(err) == platformclient.ErrForbidden { + return errors.New("removing policies requires an enterprise plan") + } + return errors.Wrap(err, "remove policy") + } + + fmt.Fprintf(r.w, "Policy %s removed.\n", policy.Name) + return r.w.Flush() +} diff --git a/cli/cmd/policy_update.go b/cli/cmd/policy_update.go new file mode 100644 index 000000000..1acba134f --- /dev/null +++ b/cli/cmd/policy_update.go @@ -0,0 +1,92 @@ +package cmd + +import ( + "fmt" + + "github.com/pkg/errors" + "github.com/replicatedhq/replicated/cli/print" + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/spf13/cobra" +) + +func (r *runners) InitPolicyUpdate(parent *cobra.Command) *cobra.Command { + var ( + newName string + description string + definitionFile string + outputFormat string + ) + + cmd := &cobra.Command{ + Use: "update NAME_OR_ID", + Short: "Update an RBAC policy", + Long: `Update an existing RBAC policy. + +At least one of --name, --description, or --definition must be provided. +The Admin, Read Only, Sales, and Support policies cannot be updated. +Vendors not on an enterprise plan cannot update policies.`, + Example: ` # Update a policy's definition from a file + replicated policy update "My Policy" --definition updated-policy.json + + # Rename a policy + replicated policy update "My Policy" --name "New Policy Name" + + # Update a policy's description and definition + replicated policy update "My Policy" --description "Updated description" --definition policy.json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return r.policyUpdate(cmd, args[0], newName, description, definitionFile, outputFormat) + }, + SilenceUsage: true, + } + parent.AddCommand(cmd) + cmd.Flags().StringVar(&newName, "name", "", "New name for the policy") + cmd.Flags().StringVar(&description, "description", "", "New description for the policy") + cmd.Flags().StringVar(&definitionFile, "definition", "", "Path to the JSON file containing the updated policy definition") + cmd.Flags().StringVarP(&outputFormat, "output", "o", "table", "The output format to use. One of: json|table") + + return cmd +} + +func (r *runners) policyUpdate(cmd *cobra.Command, nameOrID, newName, description, definitionFile, outputFormat string) error { + if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("description") && !cmd.Flags().Changed("definition") { + return errors.New("at least one of --name, --description, or --definition must be specified") + } + + existing, err := r.kotsAPI.GetPolicyByNameOrID(nameOrID) + if err != nil { + return errors.Wrap(err, "get policy") + } + + if existing.ReadOnly { + return fmt.Errorf("policy %q is read-only and cannot be updated", existing.Name) + } + + name := existing.Name + if cmd.Flags().Changed("name") { + name = newName + } + + desc := existing.Description + if cmd.Flags().Changed("description") { + desc = description + } + + definition := existing.Definition + if cmd.Flags().Changed("definition") { + definition, err = readPolicyDefinition(definitionFile) + if err != nil { + return errors.Wrap(err, "read policy definition") + } + } + + policy, err := r.kotsAPI.UpdatePolicy(existing.ID, name, desc, definition) + if err != nil { + if errors.Cause(err) == platformclient.ErrForbidden { + return errors.New("updating policies requires an enterprise plan") + } + return errors.Wrap(err, "update policy") + } + + return print.Policy(outputFormat, r.w, policy) +} diff --git a/cli/cmd/root.go b/cli/cmd/root.go index 70ac46668..226f41d34 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -498,6 +498,14 @@ func Execute(rootCmd *cobra.Command, stdin io.Reader, stdout io.Writer, stderr i apiCmd.PersistentPreRunE = preRunSetupAPIs modelCmd.PersistentPreRunE = preRunSetupAPIs + policyCmd := runCmds.InitPolicyCommand(runCmds.rootCmd) + runCmds.InitPolicyList(policyCmd) + runCmds.InitPolicyGet(policyCmd) + runCmds.InitPolicyCreate(policyCmd) + runCmds.InitPolicyUpdate(policyCmd) + runCmds.InitPolicyRm(policyCmd) + policyCmd.PersistentPreRunE = preRunSetupAPIs + // Add config command with init subcommand configCmd := runCmds.InitConfigCommand(runCmds.rootCmd) runCmds.InitInitCommand(configCmd) diff --git a/cli/print/policies.go b/cli/print/policies.go new file mode 100644 index 000000000..57ba21742 --- /dev/null +++ b/cli/print/policies.go @@ -0,0 +1,45 @@ +package print + +import ( + "encoding/json" + "fmt" + "text/tabwriter" + "text/template" + + "github.com/replicatedhq/replicated/pkg/types" +) + +var policiesTmplSrc = `ID NAME DESCRIPTION READ ONLY +{{ range . -}} +{{ .ID }} {{ .Name }} {{ .Description }} {{ .ReadOnly }} +{{ end }}` + +var policiesTmpl = template.Must(template.New("policies").Parse(policiesTmplSrc)) + +func Policies(outputFormat string, w *tabwriter.Writer, policies []*types.Policy) error { + if outputFormat == "table" { + if err := policiesTmpl.Execute(w, policies); err != nil { + return err + } + } else if outputFormat == "json" { + b, _ := json.MarshalIndent(policies, "", " ") + if _, err := fmt.Fprintln(w, string(b)); err != nil { + return err + } + } + return w.Flush() +} + +func Policy(outputFormat string, w *tabwriter.Writer, policy *types.Policy) error { + if outputFormat == "table" { + if err := policiesTmpl.Execute(w, []*types.Policy{policy}); err != nil { + return err + } + } else if outputFormat == "json" { + b, _ := json.MarshalIndent(policy, "", " ") + if _, err := fmt.Fprintln(w, string(b)); err != nil { + return err + } + } + return w.Flush() +} diff --git a/pkg/kotsclient/policy_create.go b/pkg/kotsclient/policy_create.go new file mode 100644 index 000000000..5cb261ad5 --- /dev/null +++ b/pkg/kotsclient/policy_create.go @@ -0,0 +1,31 @@ +package kotsclient + +import ( + "context" + "net/http" + + "github.com/replicatedhq/replicated/pkg/types" +) + +type CreatePolicyRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Definition string `json:"definition"` +} + +type CreatePolicyResponse struct { + Policy *types.Policy `json:"policy"` +} + +func (c *VendorV3Client) CreatePolicy(name, description, definition string) (*types.Policy, error) { + req := CreatePolicyRequest{ + Name: name, + Description: description, + Definition: definition, + } + resp := CreatePolicyResponse{} + if err := c.DoJSON(context.TODO(), "POST", "/v3/policy", http.StatusOK, req, &resp); err != nil { + return nil, err + } + return resp.Policy, nil +} diff --git a/pkg/kotsclient/policy_delete.go b/pkg/kotsclient/policy_delete.go new file mode 100644 index 000000000..ac2515cad --- /dev/null +++ b/pkg/kotsclient/policy_delete.go @@ -0,0 +1,12 @@ +package kotsclient + +import ( + "context" + "fmt" + "net/http" +) + +func (c *VendorV3Client) DeletePolicy(id string) error { + endpoint := fmt.Sprintf("/v3/policy/%s", id) + return c.DoJSON(context.TODO(), "DELETE", endpoint, http.StatusOK, nil, nil) +} diff --git a/pkg/kotsclient/policy_get.go b/pkg/kotsclient/policy_get.go new file mode 100644 index 000000000..87ce8327e --- /dev/null +++ b/pkg/kotsclient/policy_get.go @@ -0,0 +1,23 @@ +package kotsclient + +import ( + "fmt" + + "github.com/replicatedhq/replicated/pkg/types" +) + +// GetPolicyByNameOrID returns the policy matching the given name or ID. +func (c *VendorV3Client) GetPolicyByNameOrID(nameOrID string) (*types.Policy, error) { + policies, err := c.ListPolicies() + if err != nil { + return nil, fmt.Errorf("list policies: %w", err) + } + + for _, p := range policies { + if p.ID == nameOrID || p.Name == nameOrID { + return p, nil + } + } + + return nil, fmt.Errorf("policy %q not found", nameOrID) +} diff --git a/pkg/kotsclient/policy_update.go b/pkg/kotsclient/policy_update.go new file mode 100644 index 000000000..4f56e4ae9 --- /dev/null +++ b/pkg/kotsclient/policy_update.go @@ -0,0 +1,33 @@ +package kotsclient + +import ( + "context" + "fmt" + "net/http" + + "github.com/replicatedhq/replicated/pkg/types" +) + +type UpdatePolicyRequest struct { + Name string `json:"name"` + Description string `json:"description"` + Definition string `json:"definition"` +} + +type UpdatePolicyResponse struct { + Policy *types.Policy `json:"policy"` +} + +func (c *VendorV3Client) UpdatePolicy(id, name, description, definition string) (*types.Policy, error) { + req := UpdatePolicyRequest{ + Name: name, + Description: description, + Definition: definition, + } + resp := UpdatePolicyResponse{} + endpoint := fmt.Sprintf("/v3/policy/%s", id) + if err := c.DoJSON(context.TODO(), "PUT", endpoint, http.StatusOK, req, &resp); err != nil { + return nil, err + } + return resp.Policy, nil +} From c9b084ff9c21137b0a80fb3603e8fbb8f0475e5d Mon Sep 17 00:00:00 2001 From: Mikhail Swift Date: Tue, 28 Apr 2026 13:37:04 -0400 Subject: [PATCH 2/2] test: add policy pact tests --- cli/print/policies.go | 24 +++- pact/kotsclient/policy_test.go | 245 +++++++++++++++++++++++++++++++++ 2 files changed, 263 insertions(+), 6 deletions(-) create mode 100644 pact/kotsclient/policy_test.go diff --git a/cli/print/policies.go b/cli/print/policies.go index 57ba21742..9a5ac04e1 100644 --- a/cli/print/policies.go +++ b/cli/print/policies.go @@ -17,29 +17,41 @@ var policiesTmplSrc = `ID NAME DESCRIPTION READ ONLY var policiesTmpl = template.Must(template.New("policies").Parse(policiesTmplSrc)) func Policies(outputFormat string, w *tabwriter.Writer, policies []*types.Policy) error { - if outputFormat == "table" { + switch outputFormat { + case "table": if err := policiesTmpl.Execute(w, policies); err != nil { return err } - } else if outputFormat == "json" { - b, _ := json.MarshalIndent(policies, "", " ") + case "json": + b, err := json.MarshalIndent(policies, "", " ") + if err != nil { + return fmt.Errorf("marshal policies: %w", err) + } if _, err := fmt.Fprintln(w, string(b)); err != nil { return err } + default: + return fmt.Errorf("invalid output format: %s", outputFormat) } return w.Flush() } func Policy(outputFormat string, w *tabwriter.Writer, policy *types.Policy) error { - if outputFormat == "table" { + switch outputFormat { + case "table": if err := policiesTmpl.Execute(w, []*types.Policy{policy}); err != nil { return err } - } else if outputFormat == "json" { - b, _ := json.MarshalIndent(policy, "", " ") + case "json": + b, err := json.MarshalIndent(policy, "", " ") + if err != nil { + return fmt.Errorf("marshal policy: %w", err) + } if _, err := fmt.Fprintln(w, string(b)); err != nil { return err } + default: + return fmt.Errorf("invalid output format: %s", outputFormat) } return w.Flush() } diff --git a/pact/kotsclient/policy_test.go b/pact/kotsclient/policy_test.go new file mode 100644 index 000000000..328a9de9b --- /dev/null +++ b/pact/kotsclient/policy_test.go @@ -0,0 +1,245 @@ +package kotsclient + +import ( + "fmt" + "testing" + + "github.com/pact-foundation/pact-go/dsl" + realkotsclient "github.com/replicatedhq/replicated/pkg/kotsclient" + "github.com/replicatedhq/replicated/pkg/platformclient" + "github.com/stretchr/testify/assert" +) + +const adminPolicyDefinition = `{"v1":{"name":"Admin","resources":{"allowed":["**/*"],"denied":[]}}}` + +func Test_ListPolicies(t *testing.T) { + test := func() error { + u := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + api := platformclient.NewHTTPClient(u, "replicated-cli-list-policies-token") + client := realkotsclient.VendorV3Client{HTTPClient: *api} + + policies, err := client.ListPolicies() + assert.Nil(t, err) + assert.Len(t, policies, 1) + assert.Equal(t, "Admin", policies[0].Name) + assert.True(t, policies[0].ReadOnly) + + return nil + } + + pact.AddInteraction(). + Given("List team RBAC policies"). + UponReceiving("A request to list team RBAC policies"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/v3/policies"), + Headers: dsl.MapMatcher{ + "Authorization": dsl.String("replicated-cli-list-policies-token"), + "Content-Type": dsl.String("application/json"), + }, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Body: map[string]interface{}{ + "policies": []map[string]interface{}{ + { + "id": dsl.Like("replicated-cli-list-policies-admin"), + "teamId": dsl.Like("replicated-cli-list-policies-team"), + "name": dsl.String("Admin"), + "description": dsl.Like(""), + "definition": dsl.Like(adminPolicyDefinition), + "readOnly": true, + }, + }, + }, + }) + + if err := pact.Verify(test); err != nil { + t.Fatalf("Error on Verify: %v", err) + } +} + +func Test_GetPolicyByNameOrID(t *testing.T) { + test := func() error { + u := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + api := platformclient.NewHTTPClient(u, "replicated-cli-get-policy-token") + client := realkotsclient.VendorV3Client{HTTPClient: *api} + + policy, err := client.GetPolicyByNameOrID("Custom") + assert.Nil(t, err) + assert.Equal(t, "Custom", policy.Name) + assert.False(t, policy.ReadOnly) + + _, err = client.GetPolicyByNameOrID("Does Not Exist") + assert.Error(t, err) + + return nil + } + + pact.AddInteraction(). + Given("Get team RBAC policy by name"). + UponReceiving("A request to list policies in order to resolve one by name"). + WithRequest(dsl.Request{ + Method: "GET", + Path: dsl.String("/v3/policies"), + Headers: dsl.MapMatcher{ + "Authorization": dsl.String("replicated-cli-get-policy-token"), + "Content-Type": dsl.String("application/json"), + }, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Body: map[string]interface{}{ + "policies": []map[string]interface{}{ + { + "id": dsl.Like("replicated-cli-get-policy-custom"), + "teamId": dsl.Like("replicated-cli-get-policy-team"), + "name": dsl.String("Custom"), + "description": dsl.Like(""), + "definition": dsl.Like(adminPolicyDefinition), + "readOnly": false, + }, + }, + }, + }) + + if err := pact.Verify(test); err != nil { + t.Fatalf("Error on Verify: %v", err) + } +} + +func Test_CreatePolicy(t *testing.T) { + test := func() error { + u := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + api := platformclient.NewHTTPClient(u, "replicated-cli-create-policy-token") + client := realkotsclient.VendorV3Client{HTTPClient: *api} + + policy, err := client.CreatePolicy("New Policy", "A test policy", adminPolicyDefinition) + assert.Nil(t, err) + assert.Equal(t, "New Policy", policy.Name) + assert.Equal(t, "A test policy", policy.Description) + assert.False(t, policy.ReadOnly) + + return nil + } + + pact.AddInteraction(). + Given("Create a team RBAC policy"). + UponReceiving("A request to create a team RBAC policy"). + WithRequest(dsl.Request{ + Method: "POST", + Path: dsl.String("/v3/policy"), + Headers: dsl.MapMatcher{ + "Authorization": dsl.String("replicated-cli-create-policy-token"), + "Content-Type": dsl.String("application/json"), + }, + Body: map[string]interface{}{ + "name": "New Policy", + "description": "A test policy", + "definition": adminPolicyDefinition, + }, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Body: map[string]interface{}{ + "policy": map[string]interface{}{ + "id": dsl.Like("replicated-cli-create-policy-new"), + "teamId": dsl.Like("replicated-cli-create-policy-team"), + "name": dsl.String("New Policy"), + "description": dsl.String("A test policy"), + "definition": dsl.Like(adminPolicyDefinition), + "readOnly": false, + }, + }, + }) + + if err := pact.Verify(test); err != nil { + t.Fatalf("Error on Verify: %v", err) + } +} + +func Test_UpdatePolicy(t *testing.T) { + test := func() error { + u := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + api := platformclient.NewHTTPClient(u, "replicated-cli-update-policy-token") + client := realkotsclient.VendorV3Client{HTTPClient: *api} + + policy, err := client.UpdatePolicy( + "replicated-cli-update-policy-id", + "Renamed Policy", + "Updated description", + adminPolicyDefinition, + ) + assert.Nil(t, err) + assert.Equal(t, "Renamed Policy", policy.Name) + assert.Equal(t, "Updated description", policy.Description) + + return nil + } + + pact.AddInteraction(). + Given("Update a team RBAC policy"). + UponReceiving("A request to update a team RBAC policy"). + WithRequest(dsl.Request{ + Method: "PUT", + Path: dsl.String("/v3/policy/replicated-cli-update-policy-id"), + Headers: dsl.MapMatcher{ + "Authorization": dsl.String("replicated-cli-update-policy-token"), + "Content-Type": dsl.String("application/json"), + }, + Body: map[string]interface{}{ + "name": "Renamed Policy", + "description": "Updated description", + "definition": adminPolicyDefinition, + }, + }). + WillRespondWith(dsl.Response{ + Status: 200, + Body: map[string]interface{}{ + "policy": map[string]interface{}{ + "id": dsl.Like("replicated-cli-update-policy-id"), + "teamId": dsl.Like("replicated-cli-update-policy-team"), + "name": dsl.String("Renamed Policy"), + "description": dsl.String("Updated description"), + "definition": dsl.Like(adminPolicyDefinition), + "readOnly": false, + }, + }, + }) + + if err := pact.Verify(test); err != nil { + t.Fatalf("Error on Verify: %v", err) + } +} + +func Test_DeletePolicy(t *testing.T) { + test := func() error { + u := fmt.Sprintf("http://localhost:%d", pact.Server.Port) + api := platformclient.NewHTTPClient(u, "replicated-cli-delete-policy-token") + client := realkotsclient.VendorV3Client{HTTPClient: *api} + + err := client.DeletePolicy("replicated-cli-delete-policy-id") + assert.Nil(t, err) + + return nil + } + + pact.AddInteraction(). + Given("Delete a team RBAC policy"). + UponReceiving("A request to delete a team RBAC policy"). + WithRequest(dsl.Request{ + Method: "DELETE", + Path: dsl.String("/v3/policy/replicated-cli-delete-policy-id"), + Headers: dsl.MapMatcher{ + "Authorization": dsl.String("replicated-cli-delete-policy-token"), + "Content-Type": dsl.String("application/json"), + }, + }). + WillRespondWith(dsl.Response{ + Status: 200, + }) + + if err := pact.Verify(test); err != nil { + t.Fatalf("Error on Verify: %v", err) + } +}