Skip to content

Commit

Permalink
feat: Project resource supports client security settings (#537)
Browse files Browse the repository at this point in the history
  • Loading branch information
jianyuan authored Dec 13, 2024
1 parent 275d5dc commit 68bc3fb
Show file tree
Hide file tree
Showing 6 changed files with 473 additions and 43 deletions.
4 changes: 2 additions & 2 deletions GNUmakefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ fmt:

.PHONY: test
test:
go test -v -cover -timeout=120s -parallel=10 ./...
go test ./... -v -cover -timeout=120s -parallel=10 $(TESTARGS)

.PHONY: testacc
testacc:
TF_ACC=1 go test -v -cover -timeout 120m ./...
TF_ACC=1 go test ./... -v -cover -timeout 120m $(TESTARGS)

.PHONY: sweep
sweep:
Expand Down
13 changes: 13 additions & 0 deletions docs/resources/project.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ resource "sentry_project" "default" {

### Optional

- `client_security` (Attributes) Configure origin URLs which Sentry should accept events from. This is used for communication with clients like [sentry-javascript](https://github.com/getsentry/sentry-javascript). (see [below for nested schema](#nestedatt--client_security))
- `default_key` (Boolean) Whether to create a default key. By default, Sentry will create a key for you. If you wish to manage keys manually, set this to false and create keys using the `sentry_key` resource.
- `default_rules` (Boolean) Whether to create a default issue alert. Defaults to true where the behavior is to alert the user on every new issue.
- `digests_max_delay` (Number) The maximum amount of time (in seconds) to wait between scheduling digests for delivery.
Expand All @@ -75,6 +76,18 @@ resource "sentry_project" "default" {
- `id` (String) The ID of this resource.
- `internal_id` (String) The internal ID for this project.

<a id="nestedatt--client_security"></a>
### Nested Schema for `client_security`

Optional:

- `allowed_domains` (Set of String) A list of allowed domains. Examples: https://example.com, *, *.example.com, *:80.
- `scrape_javascript` (Boolean) Enable JavaScript source fetching. Allow Sentry to scrape missing JavaScript source context when possible.
- `security_token` (String) Security Token. Outbound requests matching Allowed Domains will have the header "{security_token_header}: {security_token}" appended.
- `security_token_header` (String) Security Token Header. Outbound requests matching Allowed Domains will have the header "{security_token_header}: {security_token}" appended.
- `verify_tls_ssl` (Boolean) Verify TLS/SSL. Outbound requests will verify TLS (sometimes known as SSL) connections.


<a id="nestedatt--filters"></a>
### Nested Schema for `filters`

Expand Down
30 changes: 30 additions & 0 deletions internal/apiclient/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,18 @@ paths:
type: string
options:
type: object
allowedDomains:
type: array
items:
type: string
scrapeJavaScript:
type: boolean
securityToken:
type: string
securityTokenHeader:
type: string
verifySSL:
type: boolean
responses:
"200":
description: OK
Expand Down Expand Up @@ -407,6 +419,11 @@ components:
- resolveAge
- fingerprintingRules
- groupingEnhancements
- allowedDomains
- scrapeJavaScript
- securityToken
- securityTokenHeader
- verifySSL
properties:
organization:
$ref: "#/components/schemas/Organization"
Expand Down Expand Up @@ -449,6 +466,19 @@ components:
type: string
groupingEnhancements:
type: string
allowedDomains:
type: array
items:
type: string
scrapeJavaScript:
type: boolean
securityToken:
type: string
securityTokenHeader:
type: string
nullable: true
verifySSL:
type: boolean
ProjectKey:
type: object
required:
Expand Down
10 changes: 10 additions & 0 deletions internal/apiclient/apiclient.gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

164 changes: 164 additions & 0 deletions internal/provider/resource_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,42 @@ func (m *ProjectFilterResourceModel) Fill(ctx context.Context, project apiclient
return
}

type ProjectClientSecurityResourceModel struct {
AllowedDomains types.Set `tfsdk:"allowed_domains"`
ScrapeJavascript types.Bool `tfsdk:"scrape_javascript"`
SecurityToken types.String `tfsdk:"security_token"`
SecurityTokenHeader types.String `tfsdk:"security_token_header"`
VerifyTlsSsl types.Bool `tfsdk:"verify_tls_ssl"`
}

func (m ProjectClientSecurityResourceModel) AttributeTypes() map[string]attr.Type {
return map[string]attr.Type{
"allowed_domains": types.SetType{ElemType: types.StringType},
"scrape_javascript": types.BoolType,
"security_token": types.StringType,
"security_token_header": types.StringType,
"verify_tls_ssl": types.BoolType,
}
}

func (m *ProjectClientSecurityResourceModel) Fill(ctx context.Context, project apiclient.Project) (diags diag.Diagnostics) {
m.AllowedDomains = types.SetValueMust(types.StringType, sliceutils.Map(func(v string) attr.Value {
return types.StringValue(v)
}, project.AllowedDomains))
m.ScrapeJavascript = types.BoolValue(project.ScrapeJavaScript)
m.SecurityToken = types.StringValue(project.SecurityToken)

if project.SecurityTokenHeader == nil {
m.SecurityTokenHeader = types.StringValue("")
} else {
m.SecurityTokenHeader = types.StringPointerValue(project.SecurityTokenHeader)
}

m.VerifyTlsSsl = types.BoolValue(project.VerifySSL)

return
}

type ProjectResourceModel struct {
Id types.String `tfsdk:"id"`
Organization types.String `tfsdk:"organization"`
Expand All @@ -110,6 +146,7 @@ type ProjectResourceModel struct {
Filters types.Object `tfsdk:"filters"`
FingerprintingRules sentrytypes.TrimmedString `tfsdk:"fingerprinting_rules"`
GroupingEnhancements sentrytypes.TrimmedString `tfsdk:"grouping_enhancements"`
ClientSecurity types.Object `tfsdk:"client_security"`
}

func (m *ProjectResourceModel) Fill(ctx context.Context, project apiclient.Project) (diags diag.Diagnostics) {
Expand Down Expand Up @@ -146,6 +183,13 @@ func (m *ProjectResourceModel) Fill(ctx context.Context, project apiclient.Proje
m.FingerprintingRules = sentrytypes.TrimmedStringValue(project.FingerprintingRules)
m.GroupingEnhancements = sentrytypes.TrimmedStringValue(project.GroupingEnhancements)

var clientSecurity ProjectClientSecurityResourceModel
diags.Append(clientSecurity.Fill(ctx, project)...)

var clientSecurityDiags diag.Diagnostics
m.ClientSecurity, clientSecurityDiags = types.ObjectValueFrom(ctx, clientSecurity.AttributeTypes(), clientSecurity)
diags.Append(clientSecurityDiags...)

return
}

Expand Down Expand Up @@ -315,6 +359,63 @@ func (r *ProjectResource) Schema(ctx context.Context, req resource.SchemaRequest
stringplanmodifier.UseStateForUnknown(),
},
},
"client_security": schema.SingleNestedAttribute{
MarkdownDescription: "Configure origin URLs which Sentry should accept events from. This is used for communication with clients like [sentry-javascript](https://github.com/getsentry/sentry-javascript).",
Optional: true,
Computed: true,
Attributes: map[string]schema.Attribute{
"allowed_domains": schema.SetAttribute{
MarkdownDescription: "A list of allowed domains. Examples: https://example.com, *, *.example.com, *:80.",
ElementType: types.StringType,
Optional: true,
Computed: true,
Validators: []validator.Set{
setvalidator.SizeAtLeast(1),
},
PlanModifiers: []planmodifier.Set{
setplanmodifier.UseStateForUnknown(),
},
},
"scrape_javascript": schema.BoolAttribute{
MarkdownDescription: "Enable JavaScript source fetching. Allow Sentry to scrape missing JavaScript source context when possible.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
"security_token": schema.StringAttribute{
MarkdownDescription: "Security Token. Outbound requests matching Allowed Domains will have the header \"{security_token_header}: {security_token}\" appended.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"security_token_header": schema.StringAttribute{
MarkdownDescription: "Security Token Header. Outbound requests matching Allowed Domains will have the header \"{security_token_header}: {security_token}\" appended.",
Optional: true,
Computed: true,
Validators: []validator.String{
stringvalidator.LengthAtMost(20),
},
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"verify_tls_ssl": schema.BoolAttribute{
MarkdownDescription: "Verify TLS/SSL. Outbound requests will verify TLS (sometimes known as SSL) connections.",
Optional: true,
Computed: true,
PlanModifiers: []planmodifier.Bool{
boolplanmodifier.UseStateForUnknown(),
},
},
},
PlanModifiers: []planmodifier.Object{
objectplanmodifier.UseStateForUnknown(),
},
},
},
}
}
Expand Down Expand Up @@ -436,6 +537,37 @@ func (r *ProjectResource) Create(ctx context.Context, req resource.CreateRequest
updateBody.GroupingEnhancements = data.GroupingEnhancements.ValueStringPointer()
}

if !data.ClientSecurity.IsUnknown() {
var clientSecurity ProjectClientSecurityResourceModel
resp.Diagnostics.Append(data.ClientSecurity.As(ctx, &clientSecurity, basetypes.ObjectAsOptions{})...)
if resp.Diagnostics.HasError() {
return
}

if !clientSecurity.AllowedDomains.IsUnknown() {
resp.Diagnostics.Append(clientSecurity.AllowedDomains.ElementsAs(ctx, &updateBody.AllowedDomains, false)...)
if resp.Diagnostics.HasError() {
return
}
}

if !clientSecurity.ScrapeJavascript.IsUnknown() {
updateBody.ScrapeJavaScript = clientSecurity.ScrapeJavascript.ValueBoolPointer()
}

if !clientSecurity.SecurityToken.IsUnknown() {
updateBody.SecurityToken = clientSecurity.SecurityToken.ValueStringPointer()
}

if !clientSecurity.SecurityTokenHeader.IsUnknown() {
updateBody.SecurityTokenHeader = clientSecurity.SecurityTokenHeader.ValueStringPointer()
}

if !clientSecurity.VerifyTlsSsl.IsUnknown() {
updateBody.VerifySSL = clientSecurity.VerifyTlsSsl.ValueBoolPointer()
}
}

httpRespUpdate, err := r.apiClient.UpdateOrganizationProjectWithResponse(
ctx,
data.Organization.ValueString(),
Expand Down Expand Up @@ -620,6 +752,38 @@ func (r *ProjectResource) Update(ctx context.Context, req resource.UpdateRequest
updateBody.GroupingEnhancements = plan.GroupingEnhancements.ValueStringPointer()
}

if !plan.ClientSecurity.Equal(state.ClientSecurity) {
var clientSecurityPlan, clientSecurityState ProjectClientSecurityResourceModel
resp.Diagnostics.Append(plan.ClientSecurity.As(ctx, &clientSecurityPlan, basetypes.ObjectAsOptions{})...)
resp.Diagnostics.Append(state.ClientSecurity.As(ctx, &clientSecurityState, basetypes.ObjectAsOptions{})...)
if resp.Diagnostics.HasError() {
return
}

if !clientSecurityPlan.AllowedDomains.Equal(clientSecurityState.AllowedDomains) {
resp.Diagnostics.Append(clientSecurityPlan.AllowedDomains.ElementsAs(ctx, &updateBody.AllowedDomains, false)...)
if resp.Diagnostics.HasError() {
return
}
}

if !clientSecurityPlan.ScrapeJavascript.Equal(clientSecurityState.ScrapeJavascript) {
updateBody.ScrapeJavaScript = clientSecurityPlan.ScrapeJavascript.ValueBoolPointer()
}

if !clientSecurityPlan.SecurityToken.Equal(clientSecurityState.SecurityToken) {
updateBody.SecurityToken = clientSecurityPlan.SecurityToken.ValueStringPointer()
}

if !clientSecurityPlan.SecurityTokenHeader.Equal(clientSecurityState.SecurityTokenHeader) {
updateBody.SecurityTokenHeader = clientSecurityPlan.SecurityTokenHeader.ValueStringPointer()
}

if !clientSecurityPlan.VerifyTlsSsl.Equal(clientSecurityState.VerifyTlsSsl) {
updateBody.VerifySSL = clientSecurityPlan.VerifyTlsSsl.ValueBoolPointer()
}
}

httpRespUpdate, err := r.apiClient.UpdateOrganizationProjectWithResponse(
ctx,
plan.Organization.ValueString(),
Expand Down
Loading

0 comments on commit 68bc3fb

Please sign in to comment.