From 2e1130e3b36c50a230bd0e962dc942b01916b853 Mon Sep 17 00:00:00 2001 From: Scott Miller Date: Wed, 29 Apr 2026 12:38:46 -0500 Subject: [PATCH] feat(provider): add coder_dlp_policy resource and agent dlp_policy field --- provider/agent.go | 7 ++++ provider/agent_test.go | 51 ++++++++++++++++++++++++ provider/dlp_policy.go | 71 +++++++++++++++++++++++++++++++++ provider/dlp_policy_test.go | 79 +++++++++++++++++++++++++++++++++++++ provider/provider.go | 1 + 5 files changed, 209 insertions(+) create mode 100644 provider/dlp_policy.go create mode 100644 provider/dlp_policy_test.go diff --git a/provider/agent.go b/provider/agent.go index dc662e4a..8c104de3 100644 --- a/provider/agent.go +++ b/provider/agent.go @@ -92,6 +92,13 @@ func agentResource() *schema.Resource { "no_user_data", }, false), }, + "dlp_policy": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "The id of a `coder_dlp_policy` resource that gates access to this agent. Omitted means no policy (permissive).", + ValidateFunc: validation.IsUUID, + }, "init_script": { Type: schema.TypeString, Computed: true, diff --git a/provider/agent_test.go b/provider/agent_test.go index 34e88f1e..fc7a25ed 100644 --- a/provider/agent_test.go +++ b/provider/agent_test.go @@ -65,6 +65,57 @@ func TestAgent(t *testing.T) { }) } +func TestAgent_DLPPolicy(t *testing.T) { + t.Parallel() + + t.Run("References", func(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + resource "coder_dlp_policy" "strict" { + ssh_access = true + } + resource "coder_agent" "new" { + os = "linux" + arch = "amd64" + dlp_policy = coder_dlp_policy.strict.id + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + policy := state.Modules[0].Resources["coder_dlp_policy.strict"] + require.NotNil(t, policy) + agent := state.Modules[0].Resources["coder_agent.new"] + require.NotNil(t, agent) + require.Equal(t, policy.Primary.Attributes["id"], agent.Primary.Attributes["dlp_policy"]) + return nil + }, + }}, + }) + }) + + t.Run("InvalidUUID", func(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + resource "coder_agent" "new" { + os = "linux" + arch = "amd64" + dlp_policy = "not-a-uuid" + } + `, + ExpectError: regexp.MustCompile(`expected "dlp_policy" to be a valid UUID`), + }}, + }) + }) +} + func TestAgent_StartupScriptBehavior(t *testing.T) { t.Parallel() diff --git a/provider/dlp_policy.go b/provider/dlp_policy.go new file mode 100644 index 00000000..1664b469 --- /dev/null +++ b/provider/dlp_policy.go @@ -0,0 +1,71 @@ +package provider + +import ( + "context" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type DLPPolicy struct { + ID string `mapstructure:"id"` + SSHAccess bool `mapstructure:"ssh_access"` + WebTerminalAccess bool `mapstructure:"web_terminal_access"` + PortForwardingAccess bool `mapstructure:"port_forwarding_access"` + AllowedApplications []string `mapstructure:"allowed_applications"` +} + +func dlpPolicyResource() *schema.Resource { + return &schema.Resource{ + SchemaVersion: 1, + + Description: "Use this resource to declare a data loss prevention policy. " + + "Reference its `id` from a `coder_agent`'s `dlp_policy` attribute to apply " + + "it to that agent. A single policy may be referenced by any number of agents.", + CreateContext: func(_ context.Context, rd *schema.ResourceData, _ any) diag.Diagnostics { + rd.SetId(uuid.NewString()) + return nil + }, + ReadContext: schema.NoopContext, + DeleteContext: schema.NoopContext, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Description: "A unique identifier for this resource.", + Computed: true, + }, + "ssh_access": { + Type: schema.TypeBool, + Description: "Whether workspace users may connect to the workspace over SSH.", + Optional: true, + Default: false, + ForceNew: true, + }, + "web_terminal_access": { + Type: schema.TypeBool, + Description: "Whether workspace users may open the in-browser web terminal.", + Optional: true, + Default: false, + ForceNew: true, + }, + "port_forwarding_access": { + Type: schema.TypeBool, + Description: "Whether workspace users may forward arbitrary TCP ports from the workspace.", + Optional: true, + Default: false, + ForceNew: true, + }, + "allowed_applications": { + Type: schema.TypeList, + Description: "Slugs of coder_app resources workspace users are allowed to access. " + + "Apps whose slugs are not in this list are blocked.", + Optional: true, + ForceNew: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + } +} diff --git a/provider/dlp_policy_test.go b/provider/dlp_policy_test.go new file mode 100644 index 00000000..d1552fe4 --- /dev/null +++ b/provider/dlp_policy_test.go @@ -0,0 +1,79 @@ +package provider_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" +) + +func TestDLPPolicy(t *testing.T) { + t.Parallel() + + t.Run("AllFieldsSet", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_dlp_policy" "test" { + ssh_access = true + web_terminal_access = true + port_forwarding_access = true + allowed_applications = ["code-server", "vscode-desktop"] + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + res := state.Modules[0].Resources["coder_dlp_policy.test"] + require.NotNil(t, res) + + require.NotEmpty(t, res.Primary.Attributes["id"]) + require.Equal(t, "true", res.Primary.Attributes["ssh_access"]) + require.Equal(t, "true", res.Primary.Attributes["web_terminal_access"]) + require.Equal(t, "true", res.Primary.Attributes["port_forwarding_access"]) + require.Equal(t, "2", res.Primary.Attributes["allowed_applications.#"]) + require.Equal(t, "code-server", res.Primary.Attributes["allowed_applications.0"]) + require.Equal(t, "vscode-desktop", res.Primary.Attributes["allowed_applications.1"]) + return nil + }, + }}, + }) + }) + + t.Run("Defaults", func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" { + } + resource "coder_dlp_policy" "test" { + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + res := state.Modules[0].Resources["coder_dlp_policy.test"] + require.NotNil(t, res) + + require.NotEmpty(t, res.Primary.Attributes["id"]) + require.Equal(t, "false", res.Primary.Attributes["ssh_access"]) + require.Equal(t, "false", res.Primary.Attributes["web_terminal_access"]) + require.Equal(t, "false", res.Primary.Attributes["port_forwarding_access"]) + // Omitting the field entirely leaves allowed_applications.# blank + // in state rather than "0"; both mean "no allowed apps". + require.Empty(t, res.Primary.Attributes["allowed_applications.#"]) + return nil + }, + }}, + }) + }) +} diff --git a/provider/provider.go b/provider/provider.go index 9346984c..0e1e8324 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -77,6 +77,7 @@ func New() *schema.Provider { "coder_env": envResource(), "coder_devcontainer": devcontainerResource(), "coder_external_agent": externalAgentResource(), + "coder_dlp_policy": dlpPolicyResource(), }, } }