Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ they will require a new major version.

## [Unreleased]

## [0.1.1] - 2026-09-10

### Added

- `CreatePolicyRequest.IsDisabled`, so a Policy can be created disabled
rather than created and then disabled in a second call. Omitting it
creates the Policy enabled, which is the API's default.
- `ConditionPropertyDeviceAttested`, the `device_attested` Policy
condition property, which matches when the Client presented a valid
X.509 certificate from one of the account's trust anchors.

## [0.1.0] - 2026-09-04

First public release. The API is complete and verified against a live
Expand Down Expand Up @@ -74,5 +85,6 @@ is no code difference to migrate: change the import path to
process-global, so sharing it would mean SDK transport changes leaking
into the rest of the binary and vice versa.

[Unreleased]: https://github.com/firezone/firezone-sdk-go/compare/v0.1.0...HEAD
[Unreleased]: https://github.com/firezone/firezone-sdk-go/compare/v0.1.1...HEAD
[0.1.1]: https://github.com/firezone/firezone-sdk-go/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/firezone/firezone-sdk-go/releases/tag/v0.1.0
2 changes: 1 addition & 1 deletion firezone.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func (b requestBody) reader() io.Reader {
// It is a constant rather than something read from build info, because
// build info reports "(devel)" whenever the module is built rather than
// consumed. Bump it as part of cutting a release - see CONTRIBUTING.md.
const Version = "0.1.0"
const Version = "0.1.1"

// defaultUserAgent identifies this SDK and its version, plus the Go
// runtime it was built with - the latter is worth having when a
Expand Down
42 changes: 42 additions & 0 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,48 @@ func TestIntegration_PolicyCRUD(t *testing.T) {
}
}

// TestIntegration_PolicyCreateDisabled checks the create-time
// is_disabled flag against the real API. A unit test can only prove the
// key is sent; the server's changeset silently drops a key it does not
// cast, so only a real create proves the Policy comes back disabled.
func TestIntegration_PolicyCreateDisabled(t *testing.T) {
c := integrationClient(t)
site := newSite(t, c)
group := newGroup(t, c)
resource := newResource(t, c, site.ID)

disabled := true
policy, err := c.Policies.Create(ctx(), &firezone.CreatePolicyRequest{
GroupID: group.ID,
ResourceID: resource.ID,
IsDisabled: &disabled,
})
if err != nil {
t.Fatalf("Create disabled: %v", err)
}
cleanup(t, "Policy "+policy.ID, func() error { return c.Policies.Delete(ctx(), policy.ID) })

if !policy.IsDisabled {
t.Error("IsDisabled = false on the created Policy, want true")
}

fetched, err := c.Policies.Get(ctx(), policy.ID)
if err != nil {
t.Fatalf("Get: %v", err)
}
if !fetched.IsDisabled {
t.Error("IsDisabled = false after Get, want the Policy to stay disabled")
}

enabled, err := c.Policies.Enable(ctx(), policy.ID)
if err != nil {
t.Fatalf("Enable: %v", err)
}
if enabled.IsDisabled {
t.Error("IsDisabled = true after Enable")
}
}

func TestIntegration_GroupCRUD(t *testing.T) {
c := integrationClient(t)

Expand Down
6 changes: 6 additions & 0 deletions policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const (
ConditionPropertyAuthProviderID ConditionProperty = "auth_provider_id"
ConditionPropertyCurrentUTCDatetime ConditionProperty = "current_utc_datetime"
ConditionPropertyClientVerified ConditionProperty = "client_verified"
ConditionPropertyDeviceAttested ConditionProperty = "device_attested"
)

// ConditionOperator is the comparison a policy [Condition] applies
Expand Down Expand Up @@ -60,6 +61,10 @@ const (
// [ConditionOperatorIsInDayOfWeekTimeRanges].
// - [ConditionPropertyClientVerified] with is: Values is a
// single-element list holding "true" or "false".
// - [ConditionPropertyDeviceAttested] with is: Values is a
// single-element list holding "true" or "false". "true" requires
// the Client to have presented a valid X.509 certificate from one
// of the account's trust anchors on its current connection.
type Condition struct {
Property ConditionProperty `json:"property"`
Operator ConditionOperator `json:"operator"`
Expand All @@ -84,6 +89,7 @@ type CreatePolicyRequest struct {
ResourceID string `json:"resource_id"`
Description string `json:"description,omitempty"`
FlowLogUploadsEnabled *bool `json:"flow_log_uploads_enabled,omitempty"`
IsDisabled *bool `json:"is_disabled,omitempty"`
Conditions []Condition `json:"conditions,omitempty"`
}

Expand Down
114 changes: 114 additions & 0 deletions policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,120 @@ func TestPoliciesService_Create(t *testing.T) {
}
}

func TestPoliciesService_Create_DeviceAttestedCondition(t *testing.T) {
var gotBody map[string]any
client := testutil.NewClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
decodeJSONBody(t, r, &gotBody)
testutil.JSONResponse(http.StatusCreated, map[string]any{
"data": map[string]any{
"id": "pol-1", "group_id": "group-1", "resource_id": "res-1",
"flow_log_uploads_enabled": true,
"conditions": []map[string]any{
{"property": "device_attested", "operator": "is", "values": []string{"true"}},
},
},
})(w, r)
}))

policy, err := client.Policies.Create(context.Background(), &firezone.CreatePolicyRequest{
GroupID: "group-1",
ResourceID: "res-1",
Conditions: []firezone.Condition{{
Property: firezone.ConditionPropertyDeviceAttested,
Operator: firezone.ConditionOperatorIs,
Values: []string{"true"},
}},
})
if err != nil {
t.Fatalf("Create returned error: %v", err)
}

reqPolicy, ok := gotBody["policy"].(map[string]any)
if !ok {
t.Fatalf("body[\"policy\"] = %v, want an object", gotBody["policy"])
}
conds, ok := reqPolicy["conditions"].([]any)
if !ok || len(conds) != 1 {
t.Fatalf("body policy.conditions = %v, want 1 condition", reqPolicy["conditions"])
}
cond, ok := conds[0].(map[string]any)
if !ok {
t.Fatalf("condition = %v, want an object", conds[0])
}
if cond["property"] != "device_attested" {
t.Errorf("condition.property = %v, want device_attested", cond["property"])
}

if len(policy.Conditions) != 1 || policy.Conditions[0].Property != firezone.ConditionPropertyDeviceAttested {
t.Errorf("policy.Conditions = %+v, want a single device_attested condition", policy.Conditions)
}
}

func TestPoliciesService_Create_Disabled(t *testing.T) {
var gotBody map[string]any
client := testutil.NewClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
decodeJSONBody(t, r, &gotBody)
testutil.JSONResponse(http.StatusCreated, map[string]any{
"data": map[string]any{
"id": "pol-1", "group_id": "group-1", "resource_id": "res-1",
"flow_log_uploads_enabled": true, "is_disabled": true,
},
})(w, r)
}))

disabled := true
policy, err := client.Policies.Create(context.Background(), &firezone.CreatePolicyRequest{
GroupID: "group-1",
ResourceID: "res-1",
IsDisabled: &disabled,
})
if err != nil {
t.Fatalf("Create returned error: %v", err)
}

reqPolicy, ok := gotBody["policy"].(map[string]any)
if !ok {
t.Fatalf("body[\"policy\"] = %v, want an object", gotBody["policy"])
}
if reqPolicy["is_disabled"] != true {
t.Errorf("body.policy.is_disabled = %v, want true", reqPolicy["is_disabled"])
}
if !policy.IsDisabled {
t.Error("policy.IsDisabled = false, want true")
}
}

// Omitting IsDisabled must send no key at all: the server defaults the
// field to false, and a false sent explicitly would be indistinguishable
// here but is not what "leave it to the API" means.
func TestPoliciesService_Create_OmitsIsDisabledByDefault(t *testing.T) {
var gotBody map[string]any
client := testutil.NewClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
decodeJSONBody(t, r, &gotBody)
testutil.JSONResponse(http.StatusCreated, map[string]any{
"data": map[string]any{
"id": "pol-1", "group_id": "group-1", "resource_id": "res-1",
"flow_log_uploads_enabled": true, "is_disabled": false,
},
})(w, r)
}))

if _, err := client.Policies.Create(context.Background(), &firezone.CreatePolicyRequest{
GroupID: "group-1",
ResourceID: "res-1",
}); err != nil {
t.Fatalf("Create returned error: %v", err)
}

reqPolicy, ok := gotBody["policy"].(map[string]any)
if !ok {
t.Fatalf("body[\"policy\"] = %v, want an object", gotBody["policy"])
}
if _, present := reqPolicy["is_disabled"]; present {
t.Errorf("body.policy.is_disabled = %v, want the key omitted", reqPolicy["is_disabled"])
}
}

func TestPoliciesService_Create_TimeRangeCondition(t *testing.T) {
var gotBody map[string]any
client := testutil.NewClient(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
Expand Down
Loading